@axplusb/kepler 0.0.1 → 1.0.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 (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 +98 -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,87 @@
1
+ 'use client'
2
+
3
+ import {
4
+ BarChart,
5
+ Bar,
6
+ Cell,
7
+ XAxis,
8
+ YAxis,
9
+ CartesianGrid,
10
+ Tooltip,
11
+ ResponsiveContainer,
12
+ } from 'recharts'
13
+ import { useTheme } from '@/components/theme-provider'
14
+
15
+ interface Props {
16
+ hourCounts: Record<string, number>
17
+ }
18
+
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ function CustomTooltip({ active, payload, label }: any) {
21
+ if (!active || !payload?.length) return null
22
+ const hour = parseInt(label)
23
+ const period = hour < 12 ? 'AM' : 'PM'
24
+ const h12 = hour === 0 ? 12 : hour > 12 ? hour - 12 : hour
25
+ return (
26
+ <div className="bg-card border border-border rounded-lg px-3 py-2 text-[13px]">
27
+ <p className="text-muted-foreground">{h12}:00 {period}</p>
28
+ <p className="text-primary font-bold">{payload[0].value} sessions</p>
29
+ </div>
30
+ )
31
+ }
32
+
33
+ export function PeakHoursChart({ hourCounts }: Props) {
34
+ const { theme } = useTheme()
35
+ const isDark = theme === 'dark'
36
+
37
+ const data = Array.from({ length: 24 }, (_, i) => ({
38
+ hour: String(i),
39
+ count: hourCounts[String(i)] ?? 0,
40
+ }))
41
+
42
+ const sorted = [...data].sort((a, b) => b.count - a.count)
43
+ const top3Hours = new Set(sorted.slice(0, 3).map(d => d.hour))
44
+
45
+ // Light mode: bright amber for top, soft amber for rest
46
+ // Dark mode: keep the original darker tones
47
+ const topFill = isDark ? '#d97706' : '#f59e0b'
48
+ const normalFill = isDark ? '#78350f' : '#fde68a'
49
+ const strokeColor = isDark ? 'rgba(255,255,255,0.15)' : 'rgba(0,0,0,0.4)'
50
+
51
+ return (
52
+ <div>
53
+ <ResponsiveContainer width="100%" height={160}>
54
+ <BarChart data={data} margin={{ top: 4, right: 4, left: -16, bottom: 0 }}>
55
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" vertical={false} />
56
+ <XAxis
57
+ dataKey="hour"
58
+ tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }}
59
+ tickLine={false}
60
+ axisLine={false}
61
+ tickFormatter={v => {
62
+ const h = parseInt(v)
63
+ if (h === 0) return '12a'
64
+ if (h === 12) return '12p'
65
+ if (h < 12) return `${h}a`
66
+ return `${h - 12}p`
67
+ }}
68
+ />
69
+ <YAxis hide />
70
+ <Tooltip content={<CustomTooltip />} cursor={{ fill: 'rgba(180,83,9,0.08)' }} />
71
+ <Bar dataKey="count" radius={[2, 2, 0, 0]} maxBarSize={14}
72
+ stroke={strokeColor} strokeWidth={0.75}>
73
+ {data.map(d => (
74
+ <Cell
75
+ key={d.hour}
76
+ fill={top3Hours.has(d.hour) ? topFill : normalFill}
77
+ />
78
+ ))}
79
+ </Bar>
80
+ </BarChart>
81
+ </ResponsiveContainer>
82
+ <p className="text-[11px] font-mono text-muted-foreground/60 mt-1">
83
+ top 3 peak hours highlighted
84
+ </p>
85
+ </div>
86
+ )
87
+ }
@@ -0,0 +1,96 @@
1
+ 'use client'
2
+
3
+ import {
4
+ PieChart,
5
+ Pie,
6
+ Cell,
7
+ Tooltip,
8
+ ResponsiveContainer,
9
+ Legend,
10
+ } from 'recharts'
11
+ import type { ProjectSummary } from '@/types/claude'
12
+ import { formatTokens } from '@/lib/decode'
13
+
14
+ interface Props {
15
+ projects: ProjectSummary[]
16
+ }
17
+
18
+ /** Solid hex only — Recharts SVG `fill` does not resolve `var(--*)` reliably (shows as black). */
19
+ const PROJECT_COLORS = [
20
+ '#d97706',
21
+ '#16a34a',
22
+ '#2563eb',
23
+ '#ea580c',
24
+ '#34d399',
25
+ '#64748b',
26
+ ]
27
+
28
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
29
+ function CustomTooltip({ active, payload }: any) {
30
+ if (!active || !payload?.length) return null
31
+ const { name, value } = payload[0]
32
+ return (
33
+ <div className="bg-card border border-border rounded px-3 py-2 text-[13px]">
34
+ <p className="text-muted-foreground">{name}</p>
35
+ <p className="text-foreground font-bold">{formatTokens(value)} tokens</p>
36
+ </div>
37
+ )
38
+ }
39
+
40
+ export function ProjectActivityDonut({ projects }: Props) {
41
+ const totalTokens = projects.reduce(
42
+ (s, p) => s + (p.input_tokens ?? 0) + (p.output_tokens ?? 0),
43
+ 0
44
+ )
45
+
46
+ const data = projects
47
+ .slice(0, 5)
48
+ .map((p) => ({
49
+ name: p.display_name,
50
+ value: (p.input_tokens ?? 0) + (p.output_tokens ?? 0),
51
+ }))
52
+ .filter((d) => d.value > 0)
53
+
54
+ const othersTokens = totalTokens - data.reduce((s, d) => s + d.value, 0)
55
+ if (othersTokens > 0) {
56
+ data.push({ name: 'others', value: othersTokens })
57
+ }
58
+
59
+ if (data.length === 0) {
60
+ return (
61
+ <div className="flex items-center justify-center h-48 text-muted-foreground text-sm">
62
+ no project data
63
+ </div>
64
+ )
65
+ }
66
+
67
+ return (
68
+ <ResponsiveContainer width="100%" height={220}>
69
+ <PieChart margin={{ top: 4, right: 4, bottom: 4, left: 4 }}>
70
+ <Pie
71
+ data={data}
72
+ cx="50%"
73
+ cy="50%"
74
+ innerRadius={50}
75
+ outerRadius={80}
76
+ paddingAngle={2}
77
+ dataKey="value"
78
+ strokeWidth={0}
79
+ >
80
+ {data.map((_, i) => (
81
+ <Cell key={i} fill={PROJECT_COLORS[i % PROJECT_COLORS.length]} />
82
+ ))}
83
+ </Pie>
84
+ <Tooltip content={<CustomTooltip />} />
85
+ <Legend
86
+ iconType="circle"
87
+ iconSize={8}
88
+ wrapperStyle={{ fontSize: 12 }}
89
+ formatter={(value) => (
90
+ <span style={{ color: 'var(--muted-foreground)', fontSize: 12 }}>{value}</span>
91
+ )}
92
+ />
93
+ </PieChart>
94
+ </ResponsiveContainer>
95
+ )
96
+ }
@@ -0,0 +1,102 @@
1
+ 'use client'
2
+
3
+ import { TrendingUp, TrendingDown } from 'lucide-react'
4
+ import { LineChart, Line, ResponsiveContainer } from 'recharts'
5
+ import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
6
+ import { Badge } from '@/components/ui/badge'
7
+ import { useTheme } from '@/components/theme-provider'
8
+
9
+ interface StatCardProps {
10
+ title: string
11
+ value: string
12
+ description?: string
13
+ /** Percentage change vs previous period: positive = up, negative = down */
14
+ trend?: number
15
+ /** Raw values for sparkline (last N days) */
16
+ sparkData?: number[]
17
+ accentColor?: string
18
+ }
19
+
20
+ /** Recharts sets SVG stroke/fill from strings; `var(--*)` often does not resolve on SVG → black. */
21
+ function resolveChartColor(accentColor: string | undefined, theme: 'light' | 'dark'): string {
22
+ if (!accentColor) return theme === 'light' ? '#f97316' : '#d97706'
23
+ switch (accentColor) {
24
+ case 'var(--viz-sky)':
25
+ return theme === 'light' ? '#1d4ed8' : '#60a5fa'
26
+ case 'var(--foreground)':
27
+ return theme === 'light' ? '#18181b' : '#e8eaed'
28
+ default:
29
+ return accentColor
30
+ }
31
+ }
32
+
33
+ export function StatCard({ title, value, description, trend, sparkData, accentColor }: StatCardProps) {
34
+ const { theme } = useTheme()
35
+ const resolvedAccent = resolveChartColor(accentColor, theme)
36
+ const hasTrend = trend !== undefined && !isNaN(trend)
37
+ const isUp = hasTrend && trend! >= 0
38
+ const trendColor = hasTrend
39
+ ? isUp
40
+ ? theme === 'light'
41
+ ? '#059669'
42
+ : '#34d399'
43
+ : theme === 'light'
44
+ ? '#dc2626'
45
+ : '#f87171'
46
+ : undefined
47
+ const rawSpark = sparkData ?? []
48
+ // Single point does not draw a line in Recharts; duplicate for a flat segment.
49
+ const chartData =
50
+ rawSpark.length === 1
51
+ ? [{ v: rawSpark[0]! }, { v: rawSpark[0]! }]
52
+ : rawSpark.map(v => ({ v }))
53
+
54
+ return (
55
+ <Card className="gap-3">
56
+ <CardHeader className="pb-0">
57
+ <CardDescription className="text-sm font-medium">{title}</CardDescription>
58
+ <div className="flex items-end justify-between mt-1">
59
+ <CardTitle
60
+ className="text-3xl font-bold tabular-nums leading-none"
61
+ style={{ color: resolvedAccent }}
62
+ >
63
+ {value}
64
+ </CardTitle>
65
+ {hasTrend && (
66
+ <Badge
67
+ variant="outline"
68
+ className="gap-1 text-xs font-medium"
69
+ style={{
70
+ color: trendColor,
71
+ borderColor: `${trendColor}40`,
72
+ backgroundColor: `${trendColor}12`,
73
+ }}
74
+ >
75
+ {isUp ? <TrendingUp className="w-3 h-3" /> : <TrendingDown className="w-3 h-3" />}
76
+ {Math.abs(trend!).toFixed(1)}%
77
+ </Badge>
78
+ )}
79
+ </div>
80
+ {description && (
81
+ <CardDescription className="text-xs mt-1">{description}</CardDescription>
82
+ )}
83
+ </CardHeader>
84
+ {chartData.length > 0 && (
85
+ <CardContent className="pt-0 pb-4 px-6">
86
+ <ResponsiveContainer width="100%" height={48}>
87
+ <LineChart data={chartData} margin={{ top: 4, right: 4, left: 4, bottom: 0 }}>
88
+ <Line
89
+ type="monotone"
90
+ dataKey="v"
91
+ stroke={resolvedAccent}
92
+ strokeWidth={1.5}
93
+ dot={false}
94
+ strokeOpacity={0.7}
95
+ />
96
+ </LineChart>
97
+ </ResponsiveContainer>
98
+ </CardContent>
99
+ )}
100
+ </Card>
101
+ )
102
+ }
@@ -0,0 +1,166 @@
1
+ 'use client'
2
+
3
+ import {
4
+ AreaChart,
5
+ Area,
6
+ XAxis,
7
+ YAxis,
8
+ CartesianGrid,
9
+ Tooltip,
10
+ ResponsiveContainer,
11
+ Legend,
12
+ } from 'recharts'
13
+ import type { DailyActivity } from '@/types/claude'
14
+ import { format, parseISO, subDays } from 'date-fns'
15
+
16
+ interface Props {
17
+ data: DailyActivity[]
18
+ days?: number
19
+ dateFrom?: string
20
+ dateTo?: string
21
+ }
22
+
23
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
24
+ function CustomTooltip({ active, payload, label }: any) {
25
+ if (!active || !payload?.length) return null
26
+ return (
27
+ <div className="bg-card border border-border rounded-lg px-3 py-2 text-[13px]">
28
+ <p className="text-muted-foreground mb-1">{label}</p>
29
+ {payload.map((p: { name: string; value: number; color: string }) => (
30
+ <p key={p.name} style={{ color: p.color }}>
31
+ {p.name}: <span className="font-bold">{p.value.toLocaleString()}</span>
32
+ </p>
33
+ ))}
34
+ </div>
35
+ )
36
+ }
37
+
38
+ function toIsoDate(val: string): string | null {
39
+ const m = val.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})$/)
40
+ if (m) {
41
+ const [, month, day, year] = m
42
+ return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`
43
+ }
44
+ const iso = val.match(/^(\d{4})-(\d{2})-(\d{2})/)
45
+ return iso ? val.slice(0, 10) : null
46
+ }
47
+
48
+ function parseDateInput(val: string): Date | null {
49
+ const parsed = new Date(val)
50
+ return isNaN(parsed.getTime()) ? null : parsed
51
+ }
52
+
53
+ export function UsageOverTimeChart({ data, days = 90, dateFrom, dateTo }: Props) {
54
+ let filtered: { date: string; messages: number; sessions: number }[]
55
+
56
+ if (dateFrom && dateTo) {
57
+ const fromStr = toIsoDate(dateFrom.trim())
58
+ const toStr = toIsoDate(dateTo.trim())
59
+ if (fromStr && toStr && fromStr <= toStr) {
60
+ filtered = data
61
+ .filter((d) => {
62
+ const dataDate = /^\d{4}-\d{2}-\d{2}/.test(d.date)
63
+ ? d.date.slice(0, 10)
64
+ : toIsoDate(d.date)
65
+ if (!dataDate) return false
66
+ return dataDate >= fromStr && dataDate <= toStr
67
+ })
68
+ .map((d) => ({
69
+ date: (() => {
70
+ try {
71
+ const parsed = d.date.includes('-') ? parseISO(d.date) : new Date(d.date)
72
+ return isNaN(parsed.getTime()) ? d.date : format(parsed, 'MMM d')
73
+ } catch {
74
+ return d.date
75
+ }
76
+ })(),
77
+ messages: d.messageCount,
78
+ sessions: d.sessionCount,
79
+ }))
80
+ } else {
81
+ filtered = []
82
+ }
83
+ } else {
84
+ const cutoff = subDays(new Date(), days)
85
+ filtered = data
86
+ .filter((d) => parseISO(d.date) >= cutoff)
87
+ .map((d) => ({
88
+ date: format(parseISO(d.date), 'MMM d'),
89
+ messages: d.messageCount,
90
+ sessions: d.sessionCount,
91
+ }))
92
+ }
93
+
94
+ if (filtered.length === 0) {
95
+ const from = dateFrom && dateTo ? parseDateInput(dateFrom) : null
96
+ const to = dateFrom && dateTo ? parseDateInput(dateTo) : null
97
+ const isFuture = from && to && from > new Date()
98
+ return (
99
+ <div className="flex flex-col items-center justify-center h-48 text-muted-foreground text-sm gap-1">
100
+ <span>no data</span>
101
+ {isFuture && (
102
+ <span className="text-[12px] text-muted-foreground/60">(selected range is in the future)</span>
103
+ )}
104
+ {dateFrom && dateTo && !isFuture && (
105
+ <span className="text-[12px] text-muted-foreground/60">(no activity in this date range)</span>
106
+ )}
107
+ </div>
108
+ )
109
+ }
110
+
111
+ return (
112
+ <ResponsiveContainer width="100%" height={220}>
113
+ <AreaChart data={filtered} margin={{ top: 8, right: 8, left: -10, bottom: 0 }}>
114
+ <defs>
115
+ <linearGradient id="gradMessages" x1="0" y1="0" x2="0" y2="1">
116
+ <stop offset="5%" stopColor="#d97706" stopOpacity={0.3} />
117
+ <stop offset="95%" stopColor="#d97706" stopOpacity={0} />
118
+ </linearGradient>
119
+ <linearGradient id="gradSessions" x1="0" y1="0" x2="0" y2="1">
120
+ <stop offset="5%" stopColor="#34d399" stopOpacity={0.2} />
121
+ <stop offset="95%" stopColor="#34d399" stopOpacity={0} />
122
+ </linearGradient>
123
+ </defs>
124
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" vertical={false} />
125
+ <XAxis
126
+ dataKey="date"
127
+ tick={{ fontSize: 12, fill: 'var(--muted-foreground)' }}
128
+ tickLine={false}
129
+ axisLine={false}
130
+ interval="preserveStartEnd"
131
+ />
132
+ <YAxis
133
+ tick={{ fontSize: 12, fill: 'var(--muted-foreground)' }}
134
+ tickLine={false}
135
+ axisLine={false}
136
+ tickFormatter={v => v >= 1000 ? `${(v/1000).toFixed(0)}k` : String(v)}
137
+ />
138
+ <Tooltip content={<CustomTooltip />} />
139
+ <Legend
140
+ wrapperStyle={{ fontSize: 12, paddingTop: 8 }}
141
+ formatter={(value) => (
142
+ <span style={{ color: 'var(--muted-foreground)', fontSize: 12 }}>{value}</span>
143
+ )}
144
+ />
145
+ <Area
146
+ type="monotone"
147
+ dataKey="messages"
148
+ stroke="#d97706"
149
+ strokeWidth={2}
150
+ fill="url(#gradMessages)"
151
+ dot={false}
152
+ activeDot={{ r: 3, fill: '#fbbf24' }}
153
+ />
154
+ <Area
155
+ type="monotone"
156
+ dataKey="sessions"
157
+ stroke="#34d399"
158
+ strokeWidth={1.5}
159
+ fill="url(#gradSessions)"
160
+ dot={false}
161
+ activeDot={{ r: 3, fill: '#6ee7b7' }}
162
+ />
163
+ </AreaChart>
164
+ </ResponsiveContainer>
165
+ )
166
+ }
@@ -0,0 +1,175 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { formatCost, formatDuration, formatRelativeDate } from '@/lib/decode'
5
+ import { categoryColorMix, toolBarColor } from '@/lib/tool-categories'
6
+ import type { ProjectSummary } from '@/types/claude'
7
+ import { Card, CardContent, CardHeader } from '@/components/ui/card'
8
+ import { Badge } from '@/components/ui/badge'
9
+ import { Clock, MessageSquare, GitBranch, Plug, Bot } from 'lucide-react'
10
+
11
+ const LANG_COLORS: Record<string, string> = {
12
+ TypeScript: 'bg-blue-500/10 text-blue-700 dark:text-blue-400 border-blue-500/25',
13
+ JavaScript: 'bg-yellow-400/10 text-yellow-700 dark:text-yellow-400 border-yellow-400/30',
14
+ Python: 'bg-green-500/10 text-green-700 dark:text-green-400 border-green-500/25',
15
+ Rust: 'bg-orange-500/10 text-orange-700 dark:text-orange-400 border-orange-500/25',
16
+ Go: 'bg-cyan-500/10 text-cyan-700 dark:text-cyan-400 border-cyan-500/25',
17
+ Java: 'bg-red-500/10 text-red-700 dark:text-red-400 border-red-500/25',
18
+ 'C++': 'bg-pink-500/10 text-pink-700 dark:text-pink-400 border-pink-500/25',
19
+ C: 'bg-slate-500/10 text-slate-700 dark:text-slate-400 border-slate-500/25',
20
+ 'C#': 'bg-violet-500/10 text-violet-700 dark:text-violet-400 border-violet-500/25',
21
+ Ruby: 'bg-red-400/10 text-red-600 dark:text-red-400 border-red-400/25',
22
+ PHP: 'bg-indigo-500/10 text-indigo-700 dark:text-indigo-400 border-indigo-500/25',
23
+ Swift: 'bg-orange-400/10 text-orange-600 dark:text-orange-400 border-orange-400/25',
24
+ Kotlin: 'bg-purple-500/10 text-purple-700 dark:text-purple-400 border-purple-500/25',
25
+ CSS: 'bg-blue-700/10 dark:bg-sky-500/10 text-blue-700 dark:text-sky-400 border-blue-700/25 dark:border-sky-500/25',
26
+ HTML: 'bg-rose-500/10 text-rose-700 dark:text-rose-400 border-rose-500/25',
27
+ Shell: 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border-emerald-500/25',
28
+ Bash: 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-400 border-emerald-500/25',
29
+ Markdown: 'bg-gray-400/10 text-gray-600 dark:text-gray-400 border-gray-400/25',
30
+ JSON: 'bg-amber-400/10 text-amber-700 dark:text-amber-400 border-amber-400/25',
31
+ YAML: 'bg-lime-500/10 text-lime-700 dark:text-lime-500 border-lime-500/25',
32
+ }
33
+
34
+ const FALLBACK_PALETTE = [
35
+ 'bg-teal-500/10 text-teal-700 dark:text-teal-400 border-teal-500/25',
36
+ 'bg-fuchsia-500/10 text-fuchsia-700 dark:text-fuchsia-400 border-fuchsia-500/25',
37
+ 'bg-lime-500/10 text-lime-700 dark:text-lime-400 border-lime-500/25',
38
+ 'bg-blue-700/10 dark:bg-sky-600/10 text-blue-700 dark:text-sky-400 border-blue-700/25 dark:border-sky-600/25',
39
+ 'bg-rose-500/10 text-rose-700 dark:text-rose-400 border-rose-500/25',
40
+ 'bg-amber-600/10 text-amber-700 dark:text-amber-400 border-amber-600/25',
41
+ ]
42
+
43
+ function langColor(lang: string): string {
44
+ if (LANG_COLORS[lang]) return LANG_COLORS[lang]
45
+ let hash = 0
46
+ for (let i = 0; i < lang.length; i++) hash = (hash * 31 + lang.charCodeAt(i)) >>> 0
47
+ return FALLBACK_PALETTE[hash % FALLBACK_PALETTE.length]
48
+ }
49
+
50
+ export function ProjectCard({ project }: { project: ProjectSummary }) {
51
+ const topTools = Object.entries(project.tool_counts ?? {})
52
+ .sort(([, a], [, b]) => b - a)
53
+ .slice(0, 5)
54
+ const maxToolCount = topTools[0]?.[1] ?? 1
55
+
56
+ const topLangs = Object.entries(project.languages ?? {})
57
+ .sort(([, a], [, b]) => b - a)
58
+ .slice(0, 4)
59
+
60
+ return (
61
+ <Link href={`/projects/${project.slug}`} className="block group">
62
+ <Card className="h-full gap-0 py-0 hover:border-primary/40 transition-colors overflow-hidden">
63
+ <CardHeader className="px-4 pt-4 pb-3 gap-2">
64
+ {/* Title row */}
65
+ <div className="flex items-start justify-between gap-2">
66
+ <h3 className="text-sm font-semibold text-foreground group-hover:text-primary transition-colors truncate leading-snug">
67
+ {project.display_name}
68
+ </h3>
69
+ <span className="text-[11px] text-muted-foreground/60 whitespace-nowrap shrink-0 mt-0.5">
70
+ {formatRelativeDate(project.last_active)}
71
+ </span>
72
+ </div>
73
+
74
+ {/* Path */}
75
+ <p className="text-[11px] text-muted-foreground/50 font-mono truncate -mt-1">
76
+ {project.project_path}
77
+ </p>
78
+
79
+ {/* Language + feature badges */}
80
+ <div className="flex flex-wrap gap-1.5">
81
+ {topLangs.map(([lang]) => (
82
+ <Badge key={lang} variant="outline" className={`text-[11px] px-1.5 py-0 h-5 ${langColor(lang)}`}>
83
+ {lang}
84
+ </Badge>
85
+ ))}
86
+ {project.uses_mcp && (
87
+ <Badge variant="outline" className="text-[11px] px-1.5 py-0 h-5 bg-blue-500/10 text-blue-500 border-blue-500/20 gap-1">
88
+ <Plug className="w-2.5 h-2.5" /> MCP
89
+ </Badge>
90
+ )}
91
+ {project.uses_task_agent && (
92
+ <Badge variant="outline" className="text-[11px] px-1.5 py-0 h-5 bg-purple-500/10 text-purple-500 border-purple-500/20 gap-1">
93
+ <Bot className="w-2.5 h-2.5" /> Agent
94
+ </Badge>
95
+ )}
96
+ {project.branches.length > 0 && (
97
+ <>
98
+ <span
99
+ className="h-4 w-px shrink-0 self-center bg-border/50"
100
+ aria-hidden
101
+ />
102
+ <GitBranch className="h-3 w-3 shrink-0 self-center text-muted-foreground/45" aria-hidden />
103
+ {project.branches.slice(0, 3).map(b => (
104
+ <Badge
105
+ key={b}
106
+ variant="outline"
107
+ className="h-5 max-w-28 truncate border-border/50 px-1.5 py-0 font-mono text-[11px] text-muted-foreground/80"
108
+ title={b}
109
+ >
110
+ {b}
111
+ </Badge>
112
+ ))}
113
+ {project.branches.length > 3 && (
114
+ <span className="self-center text-[11px] text-muted-foreground/45">
115
+ +{project.branches.length - 3}
116
+ </span>
117
+ )}
118
+ </>
119
+ )}
120
+ </div>
121
+ </CardHeader>
122
+
123
+ <CardContent className="px-4 pb-4 space-y-3">
124
+ {/* Stats row */}
125
+ <div className="flex items-center gap-3 text-[12px] text-muted-foreground">
126
+ <span className="flex items-center gap-1">
127
+ <MessageSquare className="w-3 h-3" />
128
+ {project.session_count} sessions
129
+ </span>
130
+ <span className="text-border">·</span>
131
+ <span className="flex items-center gap-1">
132
+ <Clock className="w-3 h-3" />
133
+ {formatDuration(project.total_duration_minutes)}
134
+ </span>
135
+ {(project.total_lines_added ?? 0) > 0 && (
136
+ <>
137
+ <span className="text-border">·</span>
138
+ <span className="text-emerald-500 font-mono">+{project.total_lines_added.toLocaleString()}</span>
139
+ <span className="text-red-400 font-mono">-{project.total_lines_removed.toLocaleString()}</span>
140
+ </>
141
+ )}
142
+ </div>
143
+
144
+ {/* Tool bar chart */}
145
+ {topTools.length > 0 && (
146
+ <div className="space-y-1">
147
+ {topTools.map(([tool, count]) => {
148
+ const color = toolBarColor(tool)
149
+ const width = Math.max(8, Math.round((count / maxToolCount) * 100))
150
+ return (
151
+ <div key={tool} className="flex items-center gap-2 text-[11px]">
152
+ <span className="text-muted-foreground/50 w-16 truncate">{tool}</span>
153
+ <div className="flex-1 h-1.5 bg-muted rounded-full overflow-hidden">
154
+ <div
155
+ className="h-full rounded-full transition-all"
156
+ style={{ width: `${width}%`, backgroundColor: categoryColorMix(color, 58) }}
157
+ />
158
+ </div>
159
+ <span className="text-muted-foreground/40 w-7 text-right tabular-nums">{count}</span>
160
+ </div>
161
+ )
162
+ })}
163
+ </div>
164
+ )}
165
+
166
+ {/* Cost footer */}
167
+ <div className="flex items-center justify-between pt-2 border-t border-border/30">
168
+ <span className="text-[11px] text-muted-foreground/50">Est. cost</span>
169
+ <span className="text-sm font-bold text-primary tabular-nums">{formatCost(project.estimated_cost)}</span>
170
+ </div>
171
+ </CardContent>
172
+ </Card>
173
+ </Link>
174
+ )
175
+ }