@axplusb/kepler 0.0.1 → 1.0.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 (218) hide show
  1. package/README.md +82 -0
  2. package/package.json +36 -4
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/costs/route.ts +88 -0
  6. package/pulse/app/api/export/route.ts +77 -0
  7. package/pulse/app/api/history/route.ts +11 -0
  8. package/pulse/app/api/import/route.ts +31 -0
  9. package/pulse/app/api/memory/route.ts +52 -0
  10. package/pulse/app/api/plans/route.ts +9 -0
  11. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  12. package/pulse/app/api/projects/route.ts +121 -0
  13. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  14. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  15. package/pulse/app/api/sessions/route.ts +112 -0
  16. package/pulse/app/api/settings/route.ts +14 -0
  17. package/pulse/app/api/stats/route.ts +143 -0
  18. package/pulse/app/api/todos/route.ts +9 -0
  19. package/pulse/app/api/tools/route.ts +160 -0
  20. package/pulse/app/costs/page.tsx +179 -0
  21. package/pulse/app/export/page.tsx +465 -0
  22. package/pulse/app/favicon.ico +0 -0
  23. package/pulse/app/globals.css +263 -0
  24. package/pulse/app/help/page.tsx +142 -0
  25. package/pulse/app/history/page.tsx +157 -0
  26. package/pulse/app/layout.tsx +46 -0
  27. package/pulse/app/memory/page.tsx +365 -0
  28. package/pulse/app/overview-client.tsx +393 -0
  29. package/pulse/app/page.tsx +14 -0
  30. package/pulse/app/plans/page.tsx +308 -0
  31. package/pulse/app/projects/[slug]/page.tsx +390 -0
  32. package/pulse/app/projects/page.tsx +110 -0
  33. package/pulse/app/sessions/[id]/page.tsx +243 -0
  34. package/pulse/app/sessions/page.tsx +39 -0
  35. package/pulse/app/settings/page.tsx +188 -0
  36. package/pulse/app/todos/page.tsx +211 -0
  37. package/pulse/app/tools/page.tsx +249 -0
  38. package/pulse/cli.js +159 -0
  39. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  40. package/pulse/components/activity/streak-card.tsx +36 -0
  41. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  42. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  43. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  44. package/pulse/components/costs/model-token-table.tsx +60 -0
  45. package/pulse/components/global-search.tsx +193 -0
  46. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  47. package/pulse/components/layout/bottom-nav.tsx +52 -0
  48. package/pulse/components/layout/client-layout.tsx +31 -0
  49. package/pulse/components/layout/sidebar-context.tsx +50 -0
  50. package/pulse/components/layout/sidebar.tsx +182 -0
  51. package/pulse/components/layout/top-bar.tsx +121 -0
  52. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  53. package/pulse/components/overview/conversation-table.tsx +148 -0
  54. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  55. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  56. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  57. package/pulse/components/overview/stat-card.tsx +102 -0
  58. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  59. package/pulse/components/projects/project-card.tsx +175 -0
  60. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  61. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  62. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  63. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  64. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  65. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  66. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  67. package/pulse/components/sessions/session-badges.tsx +49 -0
  68. package/pulse/components/sessions/session-table.tsx +299 -0
  69. package/pulse/components/theme-provider.tsx +44 -0
  70. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  71. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  72. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  73. package/pulse/components/tools/version-history-table.tsx +32 -0
  74. package/pulse/components/ui/alert.tsx +66 -0
  75. package/pulse/components/ui/badge.tsx +48 -0
  76. package/pulse/components/ui/breadcrumb.tsx +109 -0
  77. package/pulse/components/ui/button.tsx +64 -0
  78. package/pulse/components/ui/calendar.tsx +220 -0
  79. package/pulse/components/ui/card.tsx +92 -0
  80. package/pulse/components/ui/command.tsx +158 -0
  81. package/pulse/components/ui/dialog.tsx +158 -0
  82. package/pulse/components/ui/input.tsx +21 -0
  83. package/pulse/components/ui/popover.tsx +89 -0
  84. package/pulse/components/ui/progress.tsx +31 -0
  85. package/pulse/components/ui/select.tsx +190 -0
  86. package/pulse/components/ui/separator.tsx +28 -0
  87. package/pulse/components/ui/sheet.tsx +143 -0
  88. package/pulse/components/ui/skeleton.tsx +13 -0
  89. package/pulse/components/ui/table.tsx +116 -0
  90. package/pulse/components/ui/tabs.tsx +91 -0
  91. package/pulse/components/ui/tooltip.tsx +57 -0
  92. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  93. package/pulse/components.json +23 -0
  94. package/pulse/eslint.config.mjs +18 -0
  95. package/pulse/lib/claude-reader.ts +594 -0
  96. package/pulse/lib/decode.ts +129 -0
  97. package/pulse/lib/pricing.ts +102 -0
  98. package/pulse/lib/replay-parser.ts +165 -0
  99. package/pulse/lib/tool-categories.ts +127 -0
  100. package/pulse/lib/utils.ts +6 -0
  101. package/pulse/next-env.d.ts +6 -0
  102. package/pulse/next.config.ts +16 -0
  103. package/pulse/package.json +45 -0
  104. package/pulse/postcss.config.mjs +7 -0
  105. package/pulse/public/activity.png +0 -0
  106. package/pulse/public/cc-lens.png +0 -0
  107. package/pulse/public/command-k.png +0 -0
  108. package/pulse/public/costs.png +0 -0
  109. package/pulse/public/dashboard-dark.png +0 -0
  110. package/pulse/public/dashboard-white.png +0 -0
  111. package/pulse/public/export.png +0 -0
  112. package/pulse/public/file.svg +1 -0
  113. package/pulse/public/globe.svg +1 -0
  114. package/pulse/public/next.svg +1 -0
  115. package/pulse/public/projects.png +0 -0
  116. package/pulse/public/session-chat.png +0 -0
  117. package/pulse/public/todos.png +0 -0
  118. package/pulse/public/tools.png +0 -0
  119. package/pulse/public/vercel.svg +1 -0
  120. package/pulse/public/window.svg +1 -0
  121. package/pulse/tsconfig.json +34 -0
  122. package/pulse/types/claude.ts +294 -0
  123. package/src/agents/loader.mjs +89 -0
  124. package/src/agents/parser.mjs +98 -0
  125. package/src/agents/teams.mjs +123 -0
  126. package/src/auth/oauth.mjs +220 -0
  127. package/src/auth/tarang-auth.mjs +277 -0
  128. package/src/config/cli-args.mjs +173 -0
  129. package/src/config/env.mjs +263 -0
  130. package/src/config/settings.mjs +132 -0
  131. package/src/context/ast-parser.mjs +298 -0
  132. package/src/context/bm25.mjs +85 -0
  133. package/src/context/retriever.mjs +270 -0
  134. package/src/context/skeleton.mjs +134 -0
  135. package/src/core/agent-loop.mjs +480 -0
  136. package/src/core/approval.mjs +273 -0
  137. package/src/core/backend-url.mjs +57 -0
  138. package/src/core/cache.mjs +105 -0
  139. package/src/core/callback-client.mjs +149 -0
  140. package/src/core/checkpoints.mjs +142 -0
  141. package/src/core/context-manager.mjs +198 -0
  142. package/src/core/headless.mjs +168 -0
  143. package/src/core/hooks-manager.mjs +87 -0
  144. package/src/core/jsonl-writer.mjs +351 -0
  145. package/src/core/local-agent.mjs +429 -0
  146. package/src/core/local-store.mjs +325 -0
  147. package/src/core/mode-selector.mjs +51 -0
  148. package/src/core/output-filter.mjs +177 -0
  149. package/src/core/paths.mjs +101 -0
  150. package/src/core/pricing.mjs +314 -0
  151. package/src/core/providers.mjs +219 -0
  152. package/src/core/rate-limiter.mjs +119 -0
  153. package/src/core/safety.mjs +200 -0
  154. package/src/core/scheduler.mjs +173 -0
  155. package/src/core/session-manager.mjs +317 -0
  156. package/src/core/session.mjs +143 -0
  157. package/src/core/settings-sync.mjs +85 -0
  158. package/src/core/stagnation.mjs +57 -0
  159. package/src/core/stream-client.mjs +367 -0
  160. package/src/core/streaming.mjs +182 -0
  161. package/src/core/system-prompt.mjs +135 -0
  162. package/src/core/tool-executor.mjs +725 -0
  163. package/src/hooks/engine.mjs +162 -0
  164. package/src/index.mjs +370 -0
  165. package/src/mcp/client.mjs +253 -0
  166. package/src/mcp/transport-shttp.mjs +130 -0
  167. package/src/mcp/transport-sse.mjs +131 -0
  168. package/src/mcp/transport-ws.mjs +134 -0
  169. package/src/permissions/checker.mjs +57 -0
  170. package/src/permissions/command-classifier.mjs +573 -0
  171. package/src/permissions/injection-check.mjs +60 -0
  172. package/src/permissions/path-check.mjs +102 -0
  173. package/src/permissions/prompt.mjs +73 -0
  174. package/src/permissions/sandbox.mjs +112 -0
  175. package/src/plugins/loader.mjs +138 -0
  176. package/src/skills/loader.mjs +147 -0
  177. package/src/skills/runner.mjs +55 -0
  178. package/src/telemetry/index.mjs +96 -0
  179. package/src/terminal/agents.mjs +177 -0
  180. package/src/terminal/analytics.mjs +292 -0
  181. package/src/terminal/ansi.mjs +421 -0
  182. package/src/terminal/main.mjs +150 -0
  183. package/src/terminal/repl.mjs +1484 -0
  184. package/src/terminal/tool-display.mjs +58 -0
  185. package/src/tools/agent.mjs +137 -0
  186. package/src/tools/ask-user.mjs +61 -0
  187. package/src/tools/bash.mjs +148 -0
  188. package/src/tools/cron-create.mjs +120 -0
  189. package/src/tools/cron-delete.mjs +49 -0
  190. package/src/tools/cron-list.mjs +37 -0
  191. package/src/tools/edit.mjs +82 -0
  192. package/src/tools/enter-worktree.mjs +69 -0
  193. package/src/tools/exit-worktree.mjs +57 -0
  194. package/src/tools/glob.mjs +117 -0
  195. package/src/tools/grep.mjs +129 -0
  196. package/src/tools/lint.mjs +71 -0
  197. package/src/tools/ls.mjs +58 -0
  198. package/src/tools/lsp.mjs +115 -0
  199. package/src/tools/multi-edit.mjs +94 -0
  200. package/src/tools/notebook-edit.mjs +96 -0
  201. package/src/tools/read-mcp-resource.mjs +57 -0
  202. package/src/tools/read.mjs +138 -0
  203. package/src/tools/registry.mjs +132 -0
  204. package/src/tools/remote-trigger.mjs +84 -0
  205. package/src/tools/send-message.mjs +64 -0
  206. package/src/tools/skill.mjs +52 -0
  207. package/src/tools/test-runner.mjs +49 -0
  208. package/src/tools/todo-write.mjs +68 -0
  209. package/src/tools/tool-search.mjs +77 -0
  210. package/src/tools/web-fetch.mjs +65 -0
  211. package/src/tools/web-search.mjs +89 -0
  212. package/src/tools/write.mjs +55 -0
  213. package/src/ui/banner.mjs +237 -0
  214. package/src/ui/commands.mjs +499 -0
  215. package/src/ui/formatter.mjs +379 -0
  216. package/src/ui/markdown.mjs +278 -0
  217. package/src/ui/slash-commands.mjs +258 -0
  218. package/index.js +0 -1
@@ -0,0 +1,393 @@
1
+ 'use client'
2
+
3
+ import { useState, useMemo } from 'react'
4
+ import useSWR from 'swr'
5
+ import { BarChart3, PieChart, Clock, CalendarDays } from 'lucide-react'
6
+ import { UsageOverTimeChart } from '@/components/overview/usage-over-time-chart'
7
+ import { ModelBreakdownDonut } from '@/components/overview/model-breakdown-donut'
8
+ import { ProjectActivityDonut } from '@/components/overview/project-activity-donut'
9
+ import { PeakHoursChart } from '@/components/overview/peak-hours-chart'
10
+ import { OverviewConversationTable } from '@/components/overview/conversation-table'
11
+ import { StatCard } from '@/components/overview/stat-card'
12
+ import { formatTokens, formatBytes } from '@/lib/decode'
13
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
14
+ import { Button } from '@/components/ui/button'
15
+ import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
16
+ import { Skeleton } from '@/components/ui/skeleton'
17
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
18
+ import { Calendar } from '@/components/ui/calendar'
19
+ import type { StatsCache, DailyActivity, DailyTokens } from '@/types/claude'
20
+ import type { SessionWithFacet, ProjectSummary } from '@/types/claude'
21
+ import { format, subDays } from 'date-fns'
22
+ import { useTheme } from '@/components/theme-provider'
23
+
24
+ // ─── Types ────────────────────────────────────────────────────────────────────
25
+
26
+ interface ApiResponse {
27
+ stats: StatsCache
28
+ computed: {
29
+ totalCost: number
30
+ totalCacheSavings: number
31
+ totalTokens: number
32
+ totalInputTokens: number
33
+ totalOutputTokens: number
34
+ totalCacheReadTokens: number
35
+ totalCacheWriteTokens: number
36
+ totalToolCalls: number
37
+ activeDays: number
38
+ avgSessionMinutes: number
39
+ sessionsThisMonth: number
40
+ sessionsThisWeek: number
41
+ storageBytes: number
42
+ sessionCount: number
43
+ }
44
+ }
45
+
46
+ type DatePreset = '7d' | '30d' | '90d'
47
+ type CustomRange = { from?: Date; to?: Date }
48
+
49
+ const fetcher = (url: string) =>
50
+ fetch(url).then(r => {
51
+ if (!r.ok) throw new Error(`API error ${r.status}`)
52
+ return r.json()
53
+ })
54
+
55
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
56
+
57
+ function computeTrend(
58
+ dailyActivity: DailyActivity[],
59
+ field: 'messageCount' | 'sessionCount',
60
+ days = 7,
61
+ ): number | undefined {
62
+ const sorted = [...dailyActivity].sort((a, b) => a.date.localeCompare(b.date))
63
+ const recent = sorted.slice(-days)
64
+ const previous = sorted.slice(-(days * 2), -days)
65
+ if (!recent.length || !previous.length) return undefined
66
+ const recentSum = recent.reduce((s, d) => s + (d[field] ?? 0), 0)
67
+ const prevSum = previous.reduce((s, d) => s + (d[field] ?? 0), 0)
68
+ if (prevSum === 0) return undefined
69
+ return ((recentSum - prevSum) / prevSum) * 100
70
+ }
71
+
72
+ function getActivitySpark(dailyActivity: DailyActivity[], field: 'messageCount' | 'sessionCount', days = 14): number[] {
73
+ return [...dailyActivity]
74
+ .sort((a, b) => a.date.localeCompare(b.date))
75
+ .slice(-days)
76
+ .map(d => d[field] ?? 0)
77
+ }
78
+
79
+ function getTokenSpark(tokensByDate: DailyTokens[], days = 14): number[] {
80
+ return [...tokensByDate]
81
+ .sort((a, b) => a.date.localeCompare(b.date))
82
+ .slice(-days)
83
+ .map(d => Object.values(d.tokensByModel ?? {}).reduce((s, v) => s + v, 0))
84
+ }
85
+
86
+ // ─── Component ────────────────────────────────────────────────────────────────
87
+
88
+ export function OverviewClient() {
89
+ const { theme } = useTheme()
90
+ const [datePreset, setDatePreset] = useState<DatePreset>('30d')
91
+ const [customRange, setCustomRange] = useState<CustomRange>({})
92
+ const [pickerOpen, setPickerOpen] = useState(false)
93
+
94
+ const { data, error, isLoading } = useSWR<ApiResponse>('/api/stats', fetcher, {
95
+ refreshInterval: 5_000,
96
+ })
97
+ const { data: sessionsData } = useSWR<{ sessions: SessionWithFacet[] }>('/api/sessions', fetcher, {
98
+ refreshInterval: 5_000,
99
+ })
100
+ const { data: projectsData } = useSWR<{ projects: ProjectSummary[] }>('/api/projects', fetcher, {
101
+ refreshInterval: 5_000,
102
+ })
103
+
104
+ const sessions = sessionsData?.sessions ?? []
105
+ const projects = projectsData?.projects ?? []
106
+ const projectCount = projects.length
107
+
108
+ const usingCustom = !!(customRange.from && customRange.to)
109
+ const chartDays = usingCustom
110
+ ? Math.ceil((customRange.to!.getTime() - customRange.from!.getTime()) / (24 * 60 * 60 * 1000))
111
+ : datePreset === '7d' ? 7 : datePreset === '30d' ? 30 : 90
112
+ const effectiveDateFrom = usingCustom
113
+ ? format(customRange.from!, 'MM/dd/yyyy')
114
+ : format(subDays(new Date(), chartDays), 'MM/dd/yyyy')
115
+ const effectiveDateTo = usingCustom
116
+ ? format(customRange.to!, 'MM/dd/yyyy')
117
+ : format(new Date(), 'MM/dd/yyyy')
118
+
119
+ const pickerLabel = usingCustom
120
+ ? `${format(customRange.from!, 'MMM d')} – ${format(customRange.to!, 'MMM d, yyyy')}`
121
+ : 'Pick a date'
122
+
123
+ // ── Loading ──────────────────────────────────────────────────────────────
124
+ if (isLoading || !data || !data.computed) {
125
+ return (
126
+ <div className="px-6 py-6 space-y-6">
127
+ <div className="flex items-center justify-between">
128
+ <div className="space-y-2">
129
+ <Skeleton className="h-7 w-32" />
130
+ <Skeleton className="h-4 w-56" />
131
+ </div>
132
+ <Skeleton className="h-9 w-48" />
133
+ </div>
134
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
135
+ {[...Array(4)].map((_, i) => <Skeleton key={i} className="h-32 rounded-xl" />)}
136
+ </div>
137
+ <div className="grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6">
138
+ <Skeleton className="h-72 rounded-xl" />
139
+ <Skeleton className="h-72 rounded-xl" />
140
+ </div>
141
+ </div>
142
+ )
143
+ }
144
+
145
+ if (error) {
146
+ return (
147
+ <div className="px-6 py-6 text-destructive text-sm font-mono">
148
+ ✗ error loading data: {String(error)}
149
+ </div>
150
+ )
151
+ }
152
+
153
+ const { stats, computed } = data
154
+
155
+ const inputBlue = theme === 'light' ? '#1d4ed8' : '#60a5fa'
156
+ const tokenSegs = [
157
+ { label: 'input', value: computed.totalInputTokens, color: inputBlue },
158
+ { label: 'output', value: computed.totalOutputTokens, color: '#d97706' },
159
+ { label: 'cache read', value: computed.totalCacheReadTokens, color: '#34d399' },
160
+ { label: 'cache write', value: computed.totalCacheWriteTokens, color: '#a78bfa' },
161
+ ]
162
+ const totalTokens =
163
+ computed.totalInputTokens +
164
+ computed.totalOutputTokens +
165
+ computed.totalCacheReadTokens +
166
+ computed.totalCacheWriteTokens
167
+
168
+ const tokensByDate = stats.dailyModelTokens ?? stats.tokensByDate ?? []
169
+
170
+ // Trends compare last N days vs previous N days (capped at 30 to avoid sparse data)
171
+ const trendWindow = Math.min(Math.max(chartDays, 7), 30)
172
+
173
+ return (
174
+ <div className="px-6 py-6 space-y-6 bg-background">
175
+
176
+ {/* ── Page header ───────────────────────────────────────────────────── */}
177
+ <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
178
+ <div>
179
+ <h2 className="text-2xl font-bold tracking-tight">Overview</h2>
180
+ <p className="text-sm text-muted-foreground mt-0.5">
181
+ {projectCount} projects · {formatBytes(computed.storageBytes)} stored
182
+ </p>
183
+ </div>
184
+ <div className="flex items-center gap-2">
185
+ <Tabs
186
+ value={usingCustom ? '' : datePreset}
187
+ onValueChange={v => {
188
+ setDatePreset(v as DatePreset)
189
+ setCustomRange({})
190
+ }}
191
+ >
192
+ <TabsList>
193
+ <TabsTrigger value="7d">7d</TabsTrigger>
194
+ <TabsTrigger value="30d">30d</TabsTrigger>
195
+ <TabsTrigger value="90d">90d</TabsTrigger>
196
+ </TabsList>
197
+ </Tabs>
198
+
199
+ <Popover open={pickerOpen} onOpenChange={setPickerOpen}>
200
+ <PopoverTrigger asChild>
201
+ <Button
202
+ variant={usingCustom ? 'default' : 'outline'}
203
+ size="sm"
204
+ className="gap-2"
205
+ >
206
+ <CalendarDays className="w-3.5 h-3.5" />
207
+ {pickerLabel}
208
+ </Button>
209
+ </PopoverTrigger>
210
+ <PopoverContent className="w-auto p-0" align="end">
211
+ <Calendar
212
+ mode="range"
213
+ selected={{ from: customRange.from, to: customRange.to }}
214
+ onSelect={range => {
215
+ setCustomRange({ from: range?.from, to: range?.to })
216
+ if (range?.from && range?.to) setPickerOpen(false)
217
+ }}
218
+ disabled={{ after: new Date() }}
219
+ initialFocus
220
+ />
221
+ </PopoverContent>
222
+ </Popover>
223
+
224
+ </div>
225
+ </div>
226
+
227
+ {/* ── Stat cards ────────────────────────────────────────────────────── */}
228
+ <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
229
+ <StatCard
230
+ title="Sessions"
231
+ value={computed.sessionCount.toLocaleString()}
232
+ description={`${computed.sessionsThisMonth} this month · ${computed.sessionsThisWeek} this week`}
233
+ trend={computeTrend(stats.dailyActivity, 'sessionCount', trendWindow)}
234
+ sparkData={getActivitySpark(stats.dailyActivity, 'sessionCount')}
235
+ accentColor="var(--foreground)"
236
+ />
237
+ <StatCard
238
+ title="Messages"
239
+ value={stats.totalMessages.toLocaleString()}
240
+ description={`${computed.activeDays} active days`}
241
+ trend={computeTrend(stats.dailyActivity, 'messageCount', trendWindow)}
242
+ sparkData={getActivitySpark(stats.dailyActivity, 'messageCount')}
243
+ accentColor="#d97706"
244
+ />
245
+ <StatCard
246
+ title="Tokens Used"
247
+ value={formatTokens(computed.totalTokens)}
248
+ description={`${formatTokens(computed.totalCacheReadTokens)} from cache`}
249
+ sparkData={getTokenSpark(tokensByDate)}
250
+ accentColor={inputBlue}
251
+ />
252
+ <StatCard
253
+ title="Estimated Cost"
254
+ value={`$${computed.totalCost.toFixed(2)}`}
255
+ description={`$${computed.totalCacheSavings.toFixed(2)} saved via cache`}
256
+ sparkData={getTokenSpark(tokensByDate)}
257
+ accentColor="#34d399"
258
+ />
259
+ </div>
260
+
261
+ {/* ── Main charts row ───────────────────────────────────────────────── */}
262
+ <div className="grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6">
263
+ <Card>
264
+ <CardHeader>
265
+ <div className="flex items-start justify-between">
266
+ <div>
267
+ <CardTitle>Usage Over Time</CardTitle>
268
+ <CardDescription>
269
+ Messages and sessions — last {chartDays} days
270
+ </CardDescription>
271
+ </div>
272
+ <BarChart3 className="w-4 h-4 text-muted-foreground mt-0.5" />
273
+ </div>
274
+ </CardHeader>
275
+ <CardContent>
276
+ <UsageOverTimeChart
277
+ data={stats.dailyActivity}
278
+ days={chartDays}
279
+ dateFrom={effectiveDateFrom}
280
+ dateTo={effectiveDateTo}
281
+ />
282
+ </CardContent>
283
+ </Card>
284
+
285
+ <Card>
286
+ <CardHeader>
287
+ <div className="flex items-start justify-between">
288
+ <div>
289
+ <CardTitle>Model Distribution</CardTitle>
290
+ <CardDescription>Token usage by model</CardDescription>
291
+ </div>
292
+ <PieChart className="w-4 h-4 text-muted-foreground mt-0.5" />
293
+ </div>
294
+ </CardHeader>
295
+ <CardContent>
296
+ <ModelBreakdownDonut modelUsage={stats.modelUsage} />
297
+ </CardContent>
298
+ </Card>
299
+ </div>
300
+
301
+ {/* ── Secondary charts row ──────────────────────────────────────────── */}
302
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
303
+ <Card>
304
+ <CardHeader>
305
+ <div className="flex items-start justify-between">
306
+ <div>
307
+ <CardTitle>Peak Hours</CardTitle>
308
+ <CardDescription>Activity by hour of day</CardDescription>
309
+ </div>
310
+ <Clock className="w-4 h-4 text-muted-foreground mt-0.5" />
311
+ </div>
312
+ </CardHeader>
313
+ <CardContent>
314
+ <PeakHoursChart hourCounts={stats.hourCounts ?? {}} />
315
+ </CardContent>
316
+ </Card>
317
+
318
+ <Card>
319
+ <CardHeader>
320
+ <div className="flex items-start justify-between">
321
+ <div>
322
+ <CardTitle>Project Activity</CardTitle>
323
+ <CardDescription>Distribution across projects</CardDescription>
324
+ </div>
325
+ <PieChart className="w-4 h-4 text-muted-foreground mt-0.5" />
326
+ </div>
327
+ </CardHeader>
328
+ <CardContent>
329
+ <ProjectActivityDonut projects={projects} />
330
+ </CardContent>
331
+ </Card>
332
+ </div>
333
+
334
+ {/* ── Token breakdown ───────────────────────────────────────────────── */}
335
+ <Card>
336
+ <CardHeader className="pb-3">
337
+ <CardTitle>Token Breakdown</CardTitle>
338
+ <CardDescription>Distribution across token types (all time)</CardDescription>
339
+ </CardHeader>
340
+ <CardContent className="space-y-3">
341
+ {totalTokens > 0 ? (
342
+ <>
343
+ <div className="flex h-2 rounded-full overflow-hidden w-full bg-muted/40">
344
+ {tokenSegs.map(({ label, value, color }) => (
345
+ <div
346
+ key={label}
347
+ title={`${label}: ${formatTokens(value)}`}
348
+ style={{
349
+ width: `${(value / totalTokens) * 100}%`,
350
+ minWidth: value > 0 ? 2 : 0,
351
+ backgroundColor: color,
352
+ }}
353
+ />
354
+ ))}
355
+ </div>
356
+ <div className="flex flex-wrap gap-x-8 gap-y-2">
357
+ {tokenSegs.map(({ label, value, color }) => (
358
+ <span key={label} className="inline-flex items-center gap-2">
359
+ <span
360
+ className="inline-block w-2 h-2 rounded-full shrink-0"
361
+ style={{ backgroundColor: color }}
362
+ />
363
+ <span className="text-[12px] text-muted-foreground">{label}</span>
364
+ <span className="text-[13px] font-bold tabular-nums font-mono" style={{ color }}>
365
+ {formatTokens(value)}
366
+ </span>
367
+ <span className="text-[12px] text-muted-foreground/60">
368
+ {Math.round((value / totalTokens) * 100)}%
369
+ </span>
370
+ </span>
371
+ ))}
372
+ </div>
373
+ </>
374
+ ) : (
375
+ <p className="text-sm text-muted-foreground">No token usage recorded yet.</p>
376
+ )}
377
+ </CardContent>
378
+ </Card>
379
+
380
+ {/* ── Recent sessions ───────────────────────────────────────────────── */}
381
+ <Card>
382
+ <CardHeader>
383
+ <CardTitle>Recent Sessions</CardTitle>
384
+ <CardDescription>Your latest Orca sessions</CardDescription>
385
+ </CardHeader>
386
+ <CardContent>
387
+ <OverviewConversationTable sessions={sessions} />
388
+ </CardContent>
389
+ </Card>
390
+
391
+ </div>
392
+ )
393
+ }
@@ -0,0 +1,14 @@
1
+ import { TopBar } from '@/components/layout/top-bar'
2
+ import { OverviewClient } from './overview-client'
3
+
4
+ export default function OverviewPage() {
5
+ return (
6
+ <div className="flex flex-col min-h-screen">
7
+ <TopBar
8
+ title="Orca Pulse"
9
+ subtitle="Real-time analytics for your Orca AI agent sessions"
10
+ />
11
+ <OverviewClient />
12
+ </div>
13
+ )
14
+ }