@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,299 @@
1
+ 'use client'
2
+
3
+ import { useState, useMemo, useEffect, useRef } from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+ import Link from 'next/link'
6
+ import { SessionBadges } from './session-badges'
7
+ import { formatCost, formatTokens, formatDuration, formatDate, projectDisplayName } from '@/lib/decode'
8
+ import type { SessionWithFacet } from '@/types/claude'
9
+
10
+ const PAGE_SIZE = 25
11
+
12
+ type SortKey = 'start_time' | 'duration_minutes' | 'total_messages' | 'estimated_cost' | 'tool_calls'
13
+ type SortDir = 'asc' | 'desc'
14
+
15
+ interface Props {
16
+ sessions: SessionWithFacet[]
17
+ }
18
+
19
+ function SortHeader({
20
+ label, k, sortKey, sortDir, onSort,
21
+ }: {
22
+ label: string
23
+ k: SortKey
24
+ sortKey: SortKey
25
+ sortDir: SortDir
26
+ onSort: (k: SortKey) => void
27
+ }) {
28
+ const active = sortKey === k
29
+ return (
30
+ <button
31
+ onClick={() => onSort(k)}
32
+ className={`text-left text-[12px] font-bold uppercase tracking-wider whitespace-nowrap hover:text-foreground transition-colors ${active ? 'text-primary' : 'text-muted-foreground'}`}
33
+ >
34
+ {label} {active ? (sortDir === 'desc' ? '↓' : '↑') : ''}
35
+ </button>
36
+ )
37
+ }
38
+
39
+ export function SessionTable({ sessions }: Props) {
40
+ const [sortKey, setSortKey] = useState<SortKey>('start_time')
41
+ const [sortDir, setSortDir] = useState<SortDir>('desc')
42
+ const [page, setPage] = useState(1)
43
+ const [filterCompacted, setFilterCompacted] = useState(false)
44
+ const [filterAgent, setFilterAgent] = useState(false)
45
+ const [filterMcp, setFilterMcp] = useState(false)
46
+ const [search, setSearch] = useState('')
47
+ const [focusedIdx, setFocusedIdx] = useState<number | null>(null)
48
+ const rowRefs = useRef<(HTMLTableRowElement | null)[]>([])
49
+ const router = useRouter()
50
+
51
+ const filtered = useMemo(() => {
52
+ let s = sessions
53
+ if (filterCompacted) s = s.filter(x => x.has_compaction)
54
+ if (filterAgent) s = s.filter(x => x.uses_task_agent)
55
+ if (filterMcp) s = s.filter(x => x.uses_mcp)
56
+ if (search) {
57
+ const q = search.toLowerCase()
58
+ s = s.filter(x =>
59
+ x.project_path?.toLowerCase().includes(q) ||
60
+ x.first_prompt?.toLowerCase().includes(q) ||
61
+ x.slug?.toLowerCase().includes(q)
62
+ )
63
+ }
64
+ return s
65
+ }, [sessions, filterCompacted, filterAgent, filterMcp, search])
66
+
67
+ const sorted = useMemo(() => {
68
+ return [...filtered].sort((a, b) => {
69
+ let av: number, bv: number
70
+ if (sortKey === 'start_time') {
71
+ av = new Date(a.start_time).getTime()
72
+ bv = new Date(b.start_time).getTime()
73
+ } else if (sortKey === 'total_messages') {
74
+ av = (a.user_message_count ?? 0) + (a.assistant_message_count ?? 0)
75
+ bv = (b.user_message_count ?? 0) + (b.assistant_message_count ?? 0)
76
+ } else if (sortKey === 'tool_calls') {
77
+ av = Object.values(a.tool_counts ?? {}).reduce((s, c) => s + c, 0)
78
+ bv = Object.values(b.tool_counts ?? {}).reduce((s, c) => s + c, 0)
79
+ } else {
80
+ av = (a[sortKey] as number) ?? 0
81
+ bv = (b[sortKey] as number) ?? 0
82
+ }
83
+ return sortDir === 'desc' ? bv - av : av - bv
84
+ })
85
+ }, [filtered, sortKey, sortDir])
86
+
87
+ const totalPages = Math.ceil(sorted.length / PAGE_SIZE)
88
+ const paginated = sorted.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE)
89
+
90
+ // j/k keyboard navigation for rows
91
+ useEffect(() => {
92
+ function handler(e: KeyboardEvent) {
93
+ const el = document.activeElement
94
+ const tag = el?.tagName.toLowerCase()
95
+ if (tag === 'input' || tag === 'textarea' || (el as HTMLElement)?.isContentEditable) return
96
+
97
+ if (e.key === 'j') {
98
+ e.preventDefault()
99
+ setFocusedIdx(i => {
100
+ const next = i === null ? 0 : Math.min(i + 1, paginated.length - 1)
101
+ rowRefs.current[next]?.scrollIntoView({ block: 'nearest' })
102
+ return next
103
+ })
104
+ } else if (e.key === 'k') {
105
+ e.preventDefault()
106
+ setFocusedIdx(i => {
107
+ const next = i === null ? 0 : Math.max(i - 1, 0)
108
+ rowRefs.current[next]?.scrollIntoView({ block: 'nearest' })
109
+ return next
110
+ })
111
+ } else if (e.key === 'Enter' && focusedIdx !== null) {
112
+ const s = paginated[focusedIdx]
113
+ if (s) router.push(`/sessions/${s.session_id}`)
114
+ } else if (e.key === 'Escape' && focusedIdx !== null) {
115
+ setFocusedIdx(null)
116
+ }
117
+ }
118
+ window.addEventListener('keydown', handler)
119
+ return () => window.removeEventListener('keydown', handler)
120
+ }, [focusedIdx, paginated, router])
121
+
122
+ function toggleSort(key: SortKey) {
123
+ if (sortKey === key) setSortDir(d => d === 'desc' ? 'asc' : 'desc')
124
+ else { setSortKey(key); setSortDir('desc') }
125
+ setPage(1)
126
+ setFocusedIdx(null)
127
+ }
128
+
129
+ return (
130
+ <div className="space-y-3">
131
+ {/* Filters */}
132
+ <div className="flex flex-wrap gap-2 items-center">
133
+ <input
134
+ type="text"
135
+ placeholder="Search project or prompt..."
136
+ value={search}
137
+ onChange={e => { setSearch(e.target.value); setPage(1); setFocusedIdx(null) }}
138
+ className="bg-muted border border-border rounded px-2 py-1 text-[13px] text-foreground placeholder:text-muted-foreground/50 outline-none focus:border-primary/50 w-52"
139
+ />
140
+ <label className="flex items-center gap-1.5 text-[13px] text-muted-foreground cursor-pointer hover:text-foreground transition-colors">
141
+ <input
142
+ type="checkbox"
143
+ checked={filterCompacted}
144
+ onChange={e => { setFilterCompacted(e.target.checked); setPage(1); setFocusedIdx(null) }}
145
+ className="accent-amber-500"
146
+ />
147
+ ⚡ compacted
148
+ </label>
149
+ <label className="flex items-center gap-1.5 text-[13px] text-muted-foreground cursor-pointer hover:text-foreground transition-colors">
150
+ <input
151
+ type="checkbox"
152
+ checked={filterAgent}
153
+ onChange={e => { setFilterAgent(e.target.checked); setPage(1); setFocusedIdx(null) }}
154
+ className="accent-purple-500"
155
+ />
156
+ 🤖 agent
157
+ </label>
158
+ <label className="flex items-center gap-1.5 text-[13px] text-muted-foreground cursor-pointer hover:text-foreground transition-colors">
159
+ <input
160
+ type="checkbox"
161
+ checked={filterMcp}
162
+ onChange={e => { setFilterMcp(e.target.checked); setPage(1); setFocusedIdx(null) }}
163
+ className="accent-blue-500"
164
+ />
165
+ 🔌 mcp
166
+ </label>
167
+ <span className="ml-auto text-[13px] text-muted-foreground">
168
+ {filtered.length} sessions
169
+ </span>
170
+ </div>
171
+
172
+ {/* Table */}
173
+ <div className="border border-border rounded overflow-hidden">
174
+ <div className="overflow-x-auto">
175
+ <table className="w-full text-[13px]">
176
+ <thead>
177
+ <tr className="border-b border-border bg-muted">
178
+ <th className="px-3 py-2 text-left"><SortHeader label="Date" k="start_time" sortKey={sortKey} sortDir={sortDir} onSort={toggleSort} /></th>
179
+ <th className="px-3 py-2 text-left"><span className="text-[12px] font-bold uppercase tracking-wider text-muted-foreground">Project</span></th>
180
+ <th className="px-3 py-2 text-right"><SortHeader label="Dur" k="duration_minutes" sortKey={sortKey} sortDir={sortDir} onSort={toggleSort} /></th>
181
+ <th className="px-3 py-2 text-right"><SortHeader label="Msgs" k="total_messages" sortKey={sortKey} sortDir={sortDir} onSort={toggleSort} /></th>
182
+ <th className="px-3 py-2 text-right"><SortHeader label="Tools" k="tool_calls" sortKey={sortKey} sortDir={sortDir} onSort={toggleSort} /></th>
183
+ <th className="px-3 py-2 text-right"><SortHeader label="Cost" k="estimated_cost" sortKey={sortKey} sortDir={sortDir} onSort={toggleSort} /></th>
184
+ <th className="px-3 py-2 text-left"><span className="text-[12px] font-bold uppercase tracking-wider text-muted-foreground">Flags</span></th>
185
+ </tr>
186
+ </thead>
187
+ <tbody>
188
+ {paginated.map((s, i) => {
189
+ const totalMsgs = (s.user_message_count ?? 0) + (s.assistant_message_count ?? 0)
190
+ const totalTools = Object.values(s.tool_counts ?? {}).reduce((sum, c) => sum + c, 0)
191
+ const totalTokens = (s.input_tokens ?? 0) + (s.output_tokens ?? 0)
192
+ const projectName = projectDisplayName(s.project_path ?? '')
193
+
194
+ return (
195
+ <tr
196
+ key={s.session_id}
197
+ ref={el => { rowRefs.current[i] = el }}
198
+ className={[
199
+ 'border-b border-border/50 hover:bg-muted transition-colors',
200
+ i % 2 === 0 ? '' : 'bg-muted/30',
201
+ i === focusedIdx ? 'ring-2 ring-primary ring-inset bg-muted' : '',
202
+ ].join(' ')}
203
+ >
204
+ <td className="px-3 py-2 font-mono text-muted-foreground whitespace-nowrap">
205
+ {formatDate(s.start_time)}
206
+ </td>
207
+ <td className="px-3 py-2 max-w-[200px]">
208
+ <Link
209
+ href={`/sessions/${s.session_id}`}
210
+ className="text-foreground hover:text-primary transition-colors font-medium truncate block"
211
+ title={s.project_path ?? ''}
212
+ >
213
+ {projectName}
214
+ </Link>
215
+ {s.first_prompt && (
216
+ <p className="text-muted-foreground/60 truncate text-[12px]">
217
+ {s.first_prompt.slice(0, 60)}
218
+ </p>
219
+ )}
220
+ </td>
221
+ <td className="px-3 py-2 text-right text-muted-foreground whitespace-nowrap">
222
+ {formatDuration(s.duration_minutes ?? 0)}
223
+ </td>
224
+ <td className="px-3 py-2 text-right text-muted-foreground">
225
+ {totalMsgs.toLocaleString()}
226
+ </td>
227
+ <td className="px-3 py-2 text-right text-muted-foreground">
228
+ {totalTools.toLocaleString()}
229
+ </td>
230
+ <td className="px-3 py-2 text-right font-mono text-primary">
231
+ {formatCost(s.estimated_cost)}
232
+ </td>
233
+ <td className="px-3 py-2">
234
+ <SessionBadges
235
+ has_compaction={s.has_compaction}
236
+ uses_task_agent={s.uses_task_agent}
237
+ uses_mcp={s.uses_mcp}
238
+ uses_web_search={s.uses_web_search}
239
+ uses_web_fetch={s.uses_web_fetch}
240
+ has_thinking={s.has_thinking}
241
+ />
242
+ </td>
243
+ </tr>
244
+ )
245
+ })}
246
+ {paginated.length === 0 && (
247
+ <tr>
248
+ <td colSpan={7} className="px-3 py-8 text-center text-muted-foreground/50 text-[13px]">
249
+ No sessions match filters
250
+ </td>
251
+ </tr>
252
+ )}
253
+ </tbody>
254
+ </table>
255
+ </div>
256
+ </div>
257
+
258
+ {/* Pagination */}
259
+ {totalPages > 1 && (
260
+ <div className="flex items-center justify-between text-[13px]">
261
+ <span className="text-muted-foreground">
262
+ Page {page} of {totalPages} · {sorted.length} sessions
263
+ </span>
264
+ <div className="flex gap-1">
265
+ <button
266
+ onClick={() => { setPage(p => Math.max(1, p - 1)); setFocusedIdx(null) }}
267
+ disabled={page === 1}
268
+ className="px-2 py-1 rounded border border-border text-muted-foreground hover:text-foreground hover:border-primary/40 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
269
+ >
270
+
271
+ </button>
272
+ {(() => {
273
+ const maxVisible = Math.min(5, totalPages)
274
+ const startPage = Math.max(1, Math.min(page - 2, totalPages - maxVisible + 1))
275
+ const numPages = Math.min(maxVisible, totalPages - startPage + 1)
276
+ const pages = Array.from({ length: numPages }, (_, i) => startPage + i)
277
+ return pages.map((p) => (
278
+ <button
279
+ key={p}
280
+ onClick={() => { setPage(p); setFocusedIdx(null) }}
281
+ className={`px-2 py-1 rounded border transition-colors ${p === page ? 'border-primary text-primary' : 'border-border text-muted-foreground hover:text-foreground hover:border-primary/40'}`}
282
+ >
283
+ {p}
284
+ </button>
285
+ ))
286
+ })()}
287
+ <button
288
+ onClick={() => { setPage(p => Math.min(totalPages, p + 1)); setFocusedIdx(null) }}
289
+ disabled={page === totalPages}
290
+ className="px-2 py-1 rounded border border-border text-muted-foreground hover:text-foreground hover:border-primary/40 disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
291
+ >
292
+
293
+ </button>
294
+ </div>
295
+ </div>
296
+ )}
297
+ </div>
298
+ )
299
+ }
@@ -0,0 +1,44 @@
1
+ 'use client'
2
+
3
+ import { createContext, useContext, useEffect, useState } from 'react'
4
+
5
+ type Theme = 'dark' | 'light'
6
+
7
+ const ThemeContext = createContext<{ theme: Theme; toggle: () => void }>({
8
+ theme: 'dark',
9
+ toggle: () => {},
10
+ })
11
+
12
+ export function useTheme() {
13
+ return useContext(ThemeContext)
14
+ }
15
+
16
+ export function ThemeProvider({ children }: { children: React.ReactNode }) {
17
+ const [theme, setTheme] = useState<Theme>('dark')
18
+
19
+ useEffect(() => {
20
+ const id = window.setTimeout(() => {
21
+ const stored = localStorage.getItem('theme')
22
+ const next: Theme = stored === 'light' ? 'light' : 'dark'
23
+ setTheme(next)
24
+ document.documentElement.classList.toggle('dark', next === 'dark')
25
+ }, 0)
26
+
27
+ return () => window.clearTimeout(id)
28
+ }, [])
29
+
30
+ function toggle() {
31
+ setTheme(prev => {
32
+ const next: Theme = prev === 'dark' ? 'light' : 'dark'
33
+ localStorage.setItem('theme', next)
34
+ document.documentElement.classList.toggle('dark', next === 'dark')
35
+ return next
36
+ })
37
+ }
38
+
39
+ return (
40
+ <ThemeContext.Provider value={{ theme, toggle }}>
41
+ {children}
42
+ </ThemeContext.Provider>
43
+ )
44
+ }
@@ -0,0 +1,58 @@
1
+ interface Props {
2
+ adoption: Record<string, { sessions: number; pct: number }>
3
+ totalSessions: number
4
+ }
5
+
6
+ const FEATURE_LABELS: Record<string, { label: string; icon: string }> = {
7
+ task_agents: { label: 'Task Agents', icon: '🤖' },
8
+ mcp: { label: 'MCP Servers', icon: '🔌' },
9
+ web_search: { label: 'Web Search', icon: '🔍' },
10
+ web_fetch: { label: 'Web Fetch', icon: '🌐' },
11
+ plan_mode: { label: 'Plan Mode', icon: '📋' },
12
+ git_commits: { label: 'Git Commits', icon: '📦' },
13
+ extended_thinking: { label: 'Extended Thinking', icon: '🧠' },
14
+ }
15
+
16
+ export function FeatureAdoptionTable({ adoption, totalSessions }: Props) {
17
+ const rows = Object.entries(adoption)
18
+ .map(([key, data]) => ({ key, ...data, ...FEATURE_LABELS[key] }))
19
+ .filter(r => r.label)
20
+ .sort((a, b) => b.sessions - a.sessions)
21
+
22
+ return (
23
+ <div className="overflow-x-auto">
24
+ <table className="w-full text-[13px]">
25
+ <thead>
26
+ <tr className="border-b border-border">
27
+ <th className="py-2 text-left text-[12px] font-bold text-muted-foreground uppercase tracking-wider">Feature</th>
28
+ <th className="py-2 text-right text-[12px] font-bold text-muted-foreground uppercase tracking-wider">Sessions</th>
29
+ <th className="py-2 text-right text-[12px] font-bold text-muted-foreground uppercase tracking-wider">% of Total</th>
30
+ <th className="py-2 pl-4 text-left text-[12px] font-bold text-muted-foreground uppercase tracking-wider w-32">Adoption</th>
31
+ </tr>
32
+ </thead>
33
+ <tbody>
34
+ {rows.map(r => {
35
+ const pct = (r.pct * 100).toFixed(1)
36
+ const width = Math.round(r.pct * 100)
37
+ return (
38
+ <tr key={r.key} className="border-b border-border/30 hover:bg-muted/30 transition-colors">
39
+ <td className="py-2">
40
+ <span className="mr-1.5">{r.icon}</span>
41
+ <span className="text-foreground/80">{r.label}</span>
42
+ </td>
43
+ <td className="py-2 text-right text-foreground font-bold">{r.sessions}</td>
44
+ <td className="py-2 text-right text-[#d97706]">{pct}%</td>
45
+ <td className="py-2 pl-4">
46
+ <div className="h-2 bg-muted rounded-full overflow-hidden w-24">
47
+ <div className="h-full rounded-full bg-[#d97706]/60" style={{ width: `${width}%` }} />
48
+ </div>
49
+ </td>
50
+ </tr>
51
+ )
52
+ })}
53
+ </tbody>
54
+ </table>
55
+ <p className="text-[12px] text-muted-foreground/40 mt-2">{totalSessions} total sessions analyzed</p>
56
+ </div>
57
+ )
58
+ }
@@ -0,0 +1,45 @@
1
+ import type { McpServerSummary } from '@/types/claude'
2
+
3
+ interface Props {
4
+ servers: McpServerSummary[]
5
+ }
6
+
7
+ export function McpServerPanel({ servers }: Props) {
8
+ if (servers.length === 0) {
9
+ return <p className="text-muted-foreground/50 text-[13px]">No MCP server usage detected</p>
10
+ }
11
+
12
+ return (
13
+ <div className="space-y-4">
14
+ {servers.map(srv => {
15
+ const maxCalls = srv.tools[0]?.calls ?? 1
16
+ return (
17
+ <div key={srv.server_name} className="border border-border/50 rounded-lg p-3">
18
+ <div className="flex items-center justify-between mb-2">
19
+ <span className="text-[13px] font-bold text-[#34d399]">
20
+ 🔌 {srv.server_name}
21
+ </span>
22
+ <span className="text-[12px] text-muted-foreground/60">
23
+ {srv.tools.length} tools · {srv.total_calls.toLocaleString()} calls · {srv.session_count} sessions
24
+ </span>
25
+ </div>
26
+ <div className="space-y-1">
27
+ {srv.tools.map(t => {
28
+ const width = Math.max(4, Math.round((t.calls / maxCalls) * 100))
29
+ return (
30
+ <div key={t.name} className="flex items-center gap-2 text-[12px]">
31
+ <span className="text-muted-foreground/70 w-32 truncate font-mono">{t.name}</span>
32
+ <div className="flex-1 h-1.5 bg-muted rounded-full overflow-hidden">
33
+ <div className="h-full rounded-full bg-[#34d399]/50" style={{ width: `${width}%` }} />
34
+ </div>
35
+ <span className="text-muted-foreground/50 w-12 text-right">{t.calls}</span>
36
+ </div>
37
+ )
38
+ })}
39
+ </div>
40
+ </div>
41
+ )
42
+ })}
43
+ </div>
44
+ )
45
+ }
@@ -0,0 +1,57 @@
1
+ 'use client'
2
+
3
+ import { BarChart, Bar, XAxis, YAxis, Tooltip, CartesianGrid, Cell, ResponsiveContainer } from 'recharts'
4
+ import { toolBarColor } from '@/lib/tool-categories'
5
+ import type { ToolSummary } from '@/types/claude'
6
+
7
+ interface Props {
8
+ tools: ToolSummary[]
9
+ }
10
+
11
+ export function ToolRankingChart({ tools }: Props) {
12
+ const top = tools.slice(0, 20)
13
+
14
+ return (
15
+ <div>
16
+ <h3 className="text-[13px] font-bold text-muted-foreground uppercase tracking-widest mb-3">
17
+ Tool Usage — Ranked by Total Calls
18
+ </h3>
19
+ <ResponsiveContainer width="100%" height={Math.max(200, top.length * 26)}>
20
+ <BarChart data={top} layout="vertical" margin={{ top: 0, right: 60, bottom: 0, left: 8 }}>
21
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" horizontal={false} />
22
+ <XAxis
23
+ type="number"
24
+ tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }}
25
+ tickLine={false}
26
+ axisLine={false}
27
+ tickFormatter={v => v.toLocaleString()}
28
+ />
29
+ <YAxis
30
+ type="category"
31
+ dataKey="name"
32
+ tick={{ fontSize: 12, fill: 'var(--muted-foreground)' }}
33
+ tickLine={false}
34
+ axisLine={false}
35
+ width={110}
36
+ />
37
+ <Tooltip
38
+ contentStyle={{ background: 'var(--card)', border: '1px solid var(--border)', borderRadius: 4, fontSize: 12 }}
39
+ formatter={(val: number | undefined, _name?: string, props?: { payload?: { name?: string } }) => [
40
+ (val ?? 0).toLocaleString() + ' calls',
41
+ props?.payload?.name ?? '',
42
+ ]}
43
+ />
44
+ <Bar dataKey="total_calls" radius={[0, 3, 3, 0]}>
45
+ {top.map((tool, i) => (
46
+ <Cell
47
+ key={i}
48
+ fill={toolBarColor(tool.name)}
49
+ fillOpacity={0.92}
50
+ />
51
+ ))}
52
+ </Bar>
53
+ </BarChart>
54
+ </ResponsiveContainer>
55
+ </div>
56
+ )
57
+ }
@@ -0,0 +1,32 @@
1
+ import { formatDate } from '@/lib/decode'
2
+ import type { VersionRecord } from '@/types/claude'
3
+
4
+ interface Props {
5
+ versions: VersionRecord[]
6
+ }
7
+
8
+ export function VersionHistoryTable({ versions }: Props) {
9
+ return (
10
+ <div className="overflow-x-auto">
11
+ <table className="w-full text-[13px] font-mono">
12
+ <thead>
13
+ <tr className="border-b border-border">
14
+ {['Version', 'Sessions', 'First Seen', 'Last Seen'].map(h => (
15
+ <th key={h} className={`py-2 text-[12px] font-bold text-muted-foreground uppercase tracking-wider ${h === 'Version' ? 'text-left' : 'text-right'}`}>{h}</th>
16
+ ))}
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ {versions.map((v, i) => (
21
+ <tr key={v.version} className={`border-b border-border/30 hover:bg-muted/30 transition-colors ${i === 0 ? 'text-[#34d399]' : 'text-foreground/70'}`}>
22
+ <td className="py-2 font-bold">{v.version}</td>
23
+ <td className="py-2 text-right">{v.session_count}</td>
24
+ <td className="py-2 text-right text-muted-foreground">{v.first_seen ? formatDate(v.first_seen) : '—'}</td>
25
+ <td className="py-2 text-right text-muted-foreground">{v.last_seen ? formatDate(v.last_seen) : '—'}</td>
26
+ </tr>
27
+ ))}
28
+ </tbody>
29
+ </table>
30
+ </div>
31
+ )
32
+ }
@@ -0,0 +1,66 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ const alertVariants = cva(
7
+ "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ default: "bg-card text-card-foreground",
12
+ destructive:
13
+ "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
14
+ },
15
+ },
16
+ defaultVariants: {
17
+ variant: "default",
18
+ },
19
+ }
20
+ )
21
+
22
+ function Alert({
23
+ className,
24
+ variant,
25
+ ...props
26
+ }: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
27
+ return (
28
+ <div
29
+ data-slot="alert"
30
+ role="alert"
31
+ className={cn(alertVariants({ variant }), className)}
32
+ {...props}
33
+ />
34
+ )
35
+ }
36
+
37
+ function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
38
+ return (
39
+ <div
40
+ data-slot="alert-title"
41
+ className={cn(
42
+ "col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
43
+ className
44
+ )}
45
+ {...props}
46
+ />
47
+ )
48
+ }
49
+
50
+ function AlertDescription({
51
+ className,
52
+ ...props
53
+ }: React.ComponentProps<"div">) {
54
+ return (
55
+ <div
56
+ data-slot="alert-description"
57
+ className={cn(
58
+ "col-start-2 grid justify-items-start gap-1 text-sm text-muted-foreground [&_p]:leading-relaxed",
59
+ className
60
+ )}
61
+ {...props}
62
+ />
63
+ )
64
+ }
65
+
66
+ export { Alert, AlertTitle, AlertDescription }
@@ -0,0 +1,48 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { Slot } from "radix-ui"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const badgeVariants = cva(
8
+ "inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
13
+ secondary:
14
+ "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
15
+ destructive:
16
+ "bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
17
+ outline:
18
+ "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
19
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20
+ link: "text-primary underline-offset-4 [a&]:hover:underline",
21
+ },
22
+ },
23
+ defaultVariants: {
24
+ variant: "default",
25
+ },
26
+ }
27
+ )
28
+
29
+ function Badge({
30
+ className,
31
+ variant = "default",
32
+ asChild = false,
33
+ ...props
34
+ }: React.ComponentProps<"span"> &
35
+ VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
36
+ const Comp = asChild ? Slot.Root : "span"
37
+
38
+ return (
39
+ <Comp
40
+ data-slot="badge"
41
+ data-variant={variant}
42
+ className={cn(badgeVariants({ variant }), className)}
43
+ {...props}
44
+ />
45
+ )
46
+ }
47
+
48
+ export { Badge, badgeVariants }