@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,182 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { usePathname } from 'next/navigation'
5
+ import {
6
+ LayoutDashboard, FolderOpen, MessageSquare, DollarSign,
7
+ Wrench, Activity, History, CheckSquare, FileText,
8
+ Brain, Settings, Download, HelpCircle, Moon, Sun, PanelLeftClose, PanelLeft,
9
+ } from 'lucide-react'
10
+ import { useTheme } from '@/components/theme-provider'
11
+ import { useSidebar } from '@/components/layout/sidebar-context'
12
+ import { Sheet, SheetContent } from '@/components/ui/sheet'
13
+ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
14
+ import { cn } from '@/lib/utils'
15
+
16
+ const NAV = [
17
+ { href: '/', label: 'Overview', icon: LayoutDashboard },
18
+ { href: '/projects', label: 'Projects', icon: FolderOpen },
19
+ { href: '/sessions', label: 'Sessions', icon: MessageSquare },
20
+ { href: '/costs', label: 'Costs', icon: DollarSign },
21
+ { href: '/tools', label: 'Tools', icon: Wrench },
22
+ { href: '/activity', label: 'Activity', icon: Activity },
23
+ { href: '/history', label: 'History', icon: History },
24
+ { href: '/todos', label: 'Todos', icon: CheckSquare },
25
+ { href: '/plans', label: 'Plans', icon: FileText },
26
+ { href: '/memory', label: 'Memory', icon: Brain },
27
+ { href: '/settings', label: 'Settings', icon: Settings },
28
+ { href: '/help', label: 'Help', icon: HelpCircle },
29
+ { href: '/export', label: 'Export', icon: Download },
30
+ ]
31
+
32
+ function NavItem({
33
+ href, label, icon: Icon, active, collapsed,
34
+ }: {
35
+ href: string; label: string; icon: React.ElementType; active: boolean; collapsed: boolean
36
+ }) {
37
+ const link = (
38
+ <Link
39
+ href={href}
40
+ className={cn(
41
+ 'flex items-center gap-2.5 rounded-md text-sm font-medium transition-colors relative',
42
+ collapsed ? 'justify-center p-2.5' : 'px-3 py-2.5',
43
+ active
44
+ ? 'text-sidebar-primary bg-sidebar-accent border-l-2 border-l-sidebar-primary'
45
+ : 'text-sidebar-foreground hover:text-sidebar-accent-foreground hover:bg-sidebar-accent/80',
46
+ active && collapsed && 'border-l-0',
47
+ )}
48
+ >
49
+ <Icon className={cn('w-4 h-4 shrink-0', active ? 'text-sidebar-primary' : 'text-sidebar-foreground/60')} />
50
+ {!collapsed && label}
51
+ </Link>
52
+ )
53
+
54
+ if (collapsed) {
55
+ return (
56
+ <Tooltip>
57
+ <TooltipTrigger asChild>{link}</TooltipTrigger>
58
+ <TooltipContent side="right" className="text-xs">{label}</TooltipContent>
59
+ </Tooltip>
60
+ )
61
+ }
62
+ return link
63
+ }
64
+
65
+ function SidebarContents({
66
+ collapsed = false,
67
+ onNavigate,
68
+ }: {
69
+ collapsed?: boolean
70
+ onNavigate?: () => void
71
+ }) {
72
+ const pathname = usePathname()
73
+ const { theme, toggle: toggleTheme } = useTheme()
74
+ const { toggle: toggleCollapsed } = useSidebar()
75
+
76
+ return (
77
+ <div className="flex flex-col h-full">
78
+ {/* Header */}
79
+ <div className={cn(
80
+ 'border-b border-sidebar-border flex items-center',
81
+ collapsed ? 'justify-center px-2 py-4' : 'justify-between px-4 pt-5 pb-4',
82
+ )}>
83
+ {!collapsed && (
84
+ <span
85
+ className={cn(
86
+ 'inline-block rounded-md px-2.5 py-1.5 text-[12px] leading-snug tracking-[0.06em]',
87
+ 'whitespace-nowrap select-none',
88
+ /* Light: cyan on soft tint */
89
+ 'text-[#0e7490]',
90
+ 'bg-linear-to-b from-[#06b6d4]/14 to-[#06b6d4]/6',
91
+ 'ring-1 ring-inset ring-[#06b6d4]/28',
92
+ 'shadow-[inset_0_1px_0_rgba(255,255,255,0.7),0_1px_3px_rgba(24,24,27,0.08)]',
93
+ /* Dark: cyan glow */
94
+ 'dark:text-[#22d3ee]',
95
+ 'dark:from-[#22d3ee]/18 dark:to-[#22d3ee]/8 dark:ring-[#22d3ee]/40',
96
+ 'dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.06),0_0_22px_-8px_rgba(34,211,238,0.45)]',
97
+ '[-webkit-text-stroke:0.35px_rgba(14,116,144,0.35)] dark:[-webkit-text-stroke:0.45px_#06b6d4]',
98
+ )}
99
+ style={{ fontFamily: 'var(--font-press-start)' }}
100
+ >
101
+ <span
102
+ className="dark:[text-shadow:0_1px_0_#0e4f5c,0_2px_0_#083344,0_3px_6px_rgba(0,0,0,0.35)] [text-shadow:0_1px_0_rgba(255,255,255,0.4)]"
103
+ >
104
+ Orca Pulse
105
+ </span>
106
+ </span>
107
+ )}
108
+ <button
109
+ onClick={toggleCollapsed}
110
+ aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
111
+ className="hidden md:flex p-1.5 rounded-md text-sidebar-foreground/50 hover:text-sidebar-foreground hover:bg-sidebar-accent transition-colors cursor-pointer"
112
+ >
113
+ {collapsed
114
+ ? <PanelLeft className="w-4 h-4" />
115
+ : <PanelLeftClose className="w-4 h-4" />}
116
+ </button>
117
+ </div>
118
+
119
+ {/* Nav */}
120
+ <nav className={cn('flex-1 py-4 space-y-0.5 overflow-y-auto', collapsed ? 'px-1' : 'px-3')}>
121
+ <TooltipProvider delayDuration={100}>
122
+ {NAV.map(({ href, label, icon }) => (
123
+ <div key={href} onClick={onNavigate}>
124
+ <NavItem
125
+ href={href}
126
+ label={label}
127
+ icon={icon}
128
+ active={pathname === href}
129
+ collapsed={collapsed}
130
+ />
131
+ </div>
132
+ ))}
133
+ </TooltipProvider>
134
+ </nav>
135
+
136
+ {/* Footer */}
137
+ <div className={cn(
138
+ 'border-t border-sidebar-border flex items-center',
139
+ collapsed ? 'justify-center px-2 py-3' : 'justify-between px-4 py-3',
140
+ )}>
141
+ {!collapsed && (
142
+ <span className="text-xs text-sidebar-foreground/50">
143
+ Orca Pulse
144
+ </span>
145
+ )}
146
+ <button
147
+ onClick={toggleTheme}
148
+ aria-label="Toggle theme"
149
+ className="p-1.5 rounded-md text-sidebar-foreground/50 hover:text-sidebar-foreground hover:bg-sidebar-accent transition-colors cursor-pointer"
150
+ >
151
+ {theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
152
+ </button>
153
+ </div>
154
+ </div>
155
+ )
156
+ }
157
+
158
+ export function Sidebar() {
159
+ const { collapsed, mobileOpen, setMobileOpen } = useSidebar()
160
+
161
+ return (
162
+ <>
163
+ {/* Desktop sidebar */}
164
+ <aside
165
+ className={cn(
166
+ 'hidden md:flex fixed left-0 top-0 h-screen flex-col border-r border-sidebar-border bg-sidebar z-40',
167
+ 'transition-[width] duration-300 overflow-hidden',
168
+ collapsed ? 'w-14' : 'w-56',
169
+ )}
170
+ >
171
+ <SidebarContents collapsed={collapsed} />
172
+ </aside>
173
+
174
+ {/* Mobile Sheet */}
175
+ <Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
176
+ <SheetContent side="left" className="w-56 p-0 bg-sidebar border-sidebar-border">
177
+ <SidebarContents onNavigate={() => setMobileOpen(false)} />
178
+ </SheetContent>
179
+ </Sheet>
180
+ </>
181
+ )
182
+ }
@@ -0,0 +1,121 @@
1
+ 'use client'
2
+
3
+ import { useRouter } from 'next/navigation'
4
+ import { useState, useEffect } from 'react'
5
+ import { mutate } from 'swr'
6
+ import { Search, RefreshCw, Menu } from 'lucide-react'
7
+ import { Button } from '@/components/ui/button'
8
+ import { useSidebar } from '@/components/layout/sidebar-context'
9
+ import { cn } from '@/lib/utils'
10
+
11
+ interface TopBarProps {
12
+ title: string
13
+ subtitle?: string
14
+ className?: string
15
+ }
16
+
17
+ function formatTimestamp(d: Date) {
18
+ return d.toLocaleString('en-US', {
19
+ month: 'numeric',
20
+ day: 'numeric',
21
+ year: 'numeric',
22
+ hour: 'numeric',
23
+ minute: '2-digit',
24
+ second: '2-digit',
25
+ })
26
+ }
27
+
28
+ export function TopBar({ title, subtitle, className }: TopBarProps) {
29
+ const router = useRouter()
30
+ const { setMobileOpen } = useSidebar()
31
+ const [refreshing, setRefreshing] = useState(false)
32
+ const [now, setNow] = useState<string>('')
33
+
34
+ useEffect(() => {
35
+ const update = () => setNow(formatTimestamp(new Date()))
36
+ const initial = window.setTimeout(update, 0)
37
+ const interval = window.setInterval(update, 1000)
38
+ return () => {
39
+ window.clearTimeout(initial)
40
+ window.clearInterval(interval)
41
+ }
42
+ }, [])
43
+
44
+ async function handleRefresh() {
45
+ setRefreshing(true)
46
+ await mutate(() => true, undefined, { revalidate: true })
47
+ router.refresh()
48
+ setTimeout(() => setRefreshing(false), 800)
49
+ }
50
+
51
+ return (
52
+ <header
53
+ className={cn(
54
+ 'sticky top-0 z-30 flex items-center justify-between gap-4 border-b border-border bg-background/95 py-3 backdrop-blur px-4 md:px-6',
55
+ className
56
+ )}
57
+ >
58
+ {/* Left: title + subtitle */}
59
+ <div className="min-w-0">
60
+ <h1 className="text-base font-semibold text-foreground truncate">{title}</h1>
61
+ {subtitle && (
62
+ <p className="text-xs text-muted-foreground truncate" suppressHydrationWarning>
63
+ {subtitle}{now ? ` · ${now}` : ''}
64
+ </p>
65
+ )}
66
+ {!subtitle && now && (
67
+ <p className="text-xs text-muted-foreground" suppressHydrationWarning>{now}</p>
68
+ )}
69
+ </div>
70
+
71
+ {/* Right: actions */}
72
+ <div className="flex items-center gap-2 shrink-0">
73
+ {/* Mobile hamburger */}
74
+ <Button
75
+ variant="ghost"
76
+ size="icon"
77
+ onClick={() => setMobileOpen(true)}
78
+ className="md:hidden"
79
+ aria-label="Open menu"
80
+ >
81
+ <Menu className="w-4 h-4" />
82
+ </Button>
83
+ {/* Search — desktop */}
84
+ <Button
85
+ variant="outline"
86
+ size="sm"
87
+ onClick={() => window.dispatchEvent(new CustomEvent('open-search'))}
88
+ className="hidden md:flex items-center gap-2 text-muted-foreground"
89
+ >
90
+ <Search className="w-3.5 h-3.5" />
91
+ Search
92
+ <kbd className="ml-1 text-[10px] text-muted-foreground/50 border border-border rounded px-1 font-sans">⌘K</kbd>
93
+ </Button>
94
+
95
+ {/* Search — mobile icon only */}
96
+ <Button
97
+ variant="outline"
98
+ size="icon"
99
+ onClick={() => window.dispatchEvent(new CustomEvent('open-search'))}
100
+ className="md:hidden"
101
+ aria-label="Search"
102
+ >
103
+ <Search className="w-4 h-4" />
104
+ </Button>
105
+
106
+ {/* Refresh */}
107
+ <Button
108
+ variant="outline"
109
+ size="sm"
110
+ onClick={handleRefresh}
111
+ disabled={refreshing}
112
+ className="gap-2"
113
+ aria-label="Refresh data"
114
+ >
115
+ <RefreshCw className={`w-3.5 h-3.5 ${refreshing ? 'animate-spin' : ''}`} />
116
+ <span className="hidden sm:inline">{refreshing ? 'Refreshing...' : 'Refresh'}</span>
117
+ </Button>
118
+ </div>
119
+ </header>
120
+ )
121
+ }
@@ -0,0 +1,107 @@
1
+ 'use client'
2
+
3
+ import { Fragment, useMemo } from 'react'
4
+ import { format, startOfWeek, addDays, eachWeekOfInterval } from 'date-fns'
5
+ import type { DailyActivity } from '@/types/claude'
6
+ import { useTheme } from '@/components/theme-provider'
7
+
8
+ interface Props {
9
+ data: DailyActivity[]
10
+ }
11
+
12
+ // dark: empty → dark gray → dim green → mid green → bright green
13
+ const DARK_SHADES = ['#1e2128', '#1e3a2f', '#16a34a', '#22c55e', '#86efac']
14
+ // light: empty cell reads against zinc canvas; greens stay saturated for contrast
15
+ const LIGHT_SHADES = ['#d4d4d8', '#86efac', '#4ade80', '#16a34a', '#14532d']
16
+
17
+ const DAY_LABELS = ['', 'Mon', '', 'Wed', '', 'Fri', '']
18
+
19
+ export function ActivityHeatmap({ data }: Props) {
20
+ const { theme } = useTheme()
21
+ const shades = theme === 'dark' ? DARK_SHADES : LIGHT_SHADES
22
+
23
+ function getShade(count: number, max: number): string {
24
+ if (count === 0) return shades[0]
25
+ const ratio = count / max
26
+ if (ratio < 0.2) return shades[1]
27
+ if (ratio < 0.4) return shades[2]
28
+ if (ratio < 0.7) return shades[3]
29
+ return shades[4]
30
+ }
31
+
32
+ const { weeks, maxCount } = useMemo(() => {
33
+ const countMap = new Map<string, number>()
34
+ let maxCount = 0
35
+ for (const d of data) {
36
+ countMap.set(d.date, d.messageCount)
37
+ if (d.messageCount > maxCount) maxCount = d.messageCount
38
+ }
39
+
40
+ const today = new Date()
41
+ const startDate = startOfWeek(addDays(today, -52 * 7), { weekStartsOn: 0 })
42
+ const weekStarts = eachWeekOfInterval({ start: startDate, end: today }, { weekStartsOn: 0 })
43
+ const weeks = weekStarts.map(weekStart =>
44
+ Array.from({ length: 7 }, (_, i) => {
45
+ const d = addDays(weekStart, i)
46
+ const dateStr = format(d, 'yyyy-MM-dd')
47
+ return { date: d, dateStr, count: countMap.get(dateStr) ?? 0 }
48
+ })
49
+ )
50
+
51
+ return { weeks, maxCount }
52
+ }, [data])
53
+
54
+ const nWeeks = weeks.length
55
+ const gridStyle = {
56
+ gridTemplateColumns: `minmax(1.75rem, 2rem) repeat(${nWeeks}, minmax(0, 1fr))`,
57
+ } as const
58
+
59
+ return (
60
+ <div className="w-full min-w-0">
61
+ <div
62
+ className="grid w-full gap-x-1 gap-y-1"
63
+ style={gridStyle}
64
+ >
65
+ {/* Month row */}
66
+ <div aria-hidden className="min-h-4" />
67
+ {weeks.map((week, wi) => (
68
+ <div
69
+ key={`m-${wi}`}
70
+ className="flex min-h-4 min-w-0 items-end justify-center text-[9px] leading-none text-muted-foreground/80"
71
+ >
72
+ {week[0].date.getDate() <= 7 ? format(week[0].date, 'MMM') : ''}
73
+ </div>
74
+ ))}
75
+
76
+ {/* Day rows + cells */}
77
+ {[0, 1, 2, 3, 4, 5, 6].map(di => (
78
+ <Fragment key={di}>
79
+ <div className="flex min-w-0 items-center text-[9px] leading-none text-muted-foreground/80">
80
+ {DAY_LABELS[di]}
81
+ </div>
82
+ {weeks.map((week, wi) => {
83
+ const day = week[di]
84
+ return (
85
+ <div
86
+ key={`c-${wi}-${di}`}
87
+ className="aspect-square min-h-[6px] w-full min-w-0 rounded-[3px] cursor-default transition-colors"
88
+ style={{ backgroundColor: getShade(day.count, maxCount || 1) }}
89
+ title={`${day.dateStr}: ${day.count} messages`}
90
+ />
91
+ )
92
+ })}
93
+ </Fragment>
94
+ ))}
95
+ </div>
96
+
97
+ {/* Legend */}
98
+ <div className="mt-2 flex flex-wrap items-center gap-1.5 border-t border-border/40 pt-2">
99
+ <span className="text-xs text-muted-foreground">Less</span>
100
+ {shades.map((s, i) => (
101
+ <div key={i} className="h-3 w-3 shrink-0 rounded-[3px]" style={{ backgroundColor: s }} />
102
+ ))}
103
+ <span className="text-xs text-muted-foreground">More</span>
104
+ </div>
105
+ </div>
106
+ )
107
+ }
@@ -0,0 +1,148 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState, useMemo } from 'react'
4
+ import Link from 'next/link'
5
+ import { formatTokens, formatRelativeDate, projectDisplayName } from '@/lib/decode'
6
+ import type { SessionWithFacet } from '@/types/claude'
7
+ import {
8
+ Table,
9
+ TableBody,
10
+ TableCell,
11
+ TableHead,
12
+ TableHeader,
13
+ TableRow,
14
+ } from '@/components/ui/table'
15
+ import { Badge } from '@/components/ui/badge'
16
+ import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
17
+
18
+ type FilterType = 'active' | 'recent' | 'all'
19
+
20
+ const ONE_DAY_MS = 24 * 60 * 60 * 1000
21
+ const ONE_WEEK_MS = 7 * ONE_DAY_MS
22
+
23
+ interface Props {
24
+ sessions: SessionWithFacet[]
25
+ }
26
+
27
+ function shortId(id: string): string {
28
+ return id.slice(0, 8) + '…'
29
+ }
30
+
31
+ export function OverviewConversationTable({ sessions }: Props) {
32
+ const [filter, setFilter] = useState<FilterType>('recent')
33
+ const [now, setNow] = useState<number | null>(null)
34
+
35
+ useEffect(() => {
36
+ const update = () => setNow(Date.now())
37
+ const initial = window.setTimeout(update, 0)
38
+ const interval = window.setInterval(update, 60_000)
39
+ return () => {
40
+ window.clearTimeout(initial)
41
+ window.clearInterval(interval)
42
+ }
43
+ }, [])
44
+
45
+ const filtered = useMemo(() => {
46
+ let result: SessionWithFacet[]
47
+ switch (filter) {
48
+ case 'active':
49
+ result = now === null
50
+ ? sessions
51
+ : sessions.filter(s => now - new Date(s.start_time).getTime() < ONE_DAY_MS)
52
+ break
53
+ case 'recent':
54
+ result = now === null
55
+ ? sessions
56
+ : sessions.filter(s => now - new Date(s.start_time).getTime() < ONE_WEEK_MS)
57
+ break
58
+ default:
59
+ result = sessions
60
+ }
61
+ return result.sort(
62
+ (a, b) => new Date(b.start_time).getTime() - new Date(a.start_time).getTime(),
63
+ )
64
+ }, [sessions, filter, now])
65
+
66
+ const displaySessions = filtered.slice(0, 10)
67
+
68
+ return (
69
+ <div className="space-y-4">
70
+ <Tabs value={filter} onValueChange={v => setFilter(v as FilterType)}>
71
+ <TabsList>
72
+ <TabsTrigger value="active">Active (24h)</TabsTrigger>
73
+ <TabsTrigger value="recent">Recent (7d)</TabsTrigger>
74
+ <TabsTrigger value="all">All</TabsTrigger>
75
+ </TabsList>
76
+ </Tabs>
77
+
78
+ <Table>
79
+ <TableHeader>
80
+ <TableRow>
81
+ <TableHead className="text-xs uppercase tracking-wider text-muted-foreground">Session</TableHead>
82
+ <TableHead className="text-xs uppercase tracking-wider text-muted-foreground">Project</TableHead>
83
+ <TableHead className="text-xs uppercase tracking-wider text-muted-foreground text-right">Messages</TableHead>
84
+ <TableHead className="text-xs uppercase tracking-wider text-muted-foreground text-right">Tokens</TableHead>
85
+ <TableHead className="text-xs uppercase tracking-wider text-muted-foreground">Last Active</TableHead>
86
+ <TableHead className="text-xs uppercase tracking-wider text-muted-foreground">Status</TableHead>
87
+ </TableRow>
88
+ </TableHeader>
89
+ <TableBody>
90
+ {displaySessions.map(s => {
91
+ const totalMsgs = (s.user_message_count ?? 0) + (s.assistant_message_count ?? 0)
92
+ const totalTokens = (s.input_tokens ?? 0) + (s.output_tokens ?? 0)
93
+ const projectName = projectDisplayName(s.project_path ?? '')
94
+ const isActive = now !== null && now - new Date(s.start_time).getTime() < ONE_DAY_MS
95
+
96
+ return (
97
+ <TableRow key={s.session_id}>
98
+ <TableCell className="font-mono text-muted-foreground">
99
+ <Link
100
+ href={`/sessions/${s.session_id}`}
101
+ className="hover:text-primary transition-colors"
102
+ title={s.session_id}
103
+ >
104
+ {shortId(s.session_id)}
105
+ </Link>
106
+ </TableCell>
107
+ <TableCell>
108
+ <Link
109
+ href={`/sessions/${s.session_id}`}
110
+ className="font-medium hover:text-primary transition-colors"
111
+ >
112
+ {projectName}
113
+ </Link>
114
+ </TableCell>
115
+ <TableCell className="text-right tabular-nums text-muted-foreground">
116
+ {totalMsgs.toLocaleString()}
117
+ </TableCell>
118
+ <TableCell className="text-right font-mono tabular-nums text-primary">
119
+ {formatTokens(totalTokens)}
120
+ </TableCell>
121
+ <TableCell className="text-muted-foreground text-sm">
122
+ {formatRelativeDate(s.start_time)}
123
+ </TableCell>
124
+ <TableCell>
125
+ {isActive ? (
126
+ <Badge variant="outline" className="text-[#34d399] border-[#34d399]/30 bg-[#34d399]/10 gap-1.5">
127
+ <span className="w-1.5 h-1.5 rounded-full bg-[#34d399] inline-block" />
128
+ Active
129
+ </Badge>
130
+ ) : (
131
+ <Badge variant="secondary">Completed</Badge>
132
+ )}
133
+ </TableCell>
134
+ </TableRow>
135
+ )
136
+ })}
137
+ {displaySessions.length === 0 && (
138
+ <TableRow>
139
+ <TableCell colSpan={6} className="text-center text-muted-foreground py-8">
140
+ No sessions match this filter
141
+ </TableCell>
142
+ </TableRow>
143
+ )}
144
+ </TableBody>
145
+ </Table>
146
+ </div>
147
+ )
148
+ }
@@ -0,0 +1,95 @@
1
+ 'use client'
2
+
3
+ import {
4
+ PieChart,
5
+ Pie,
6
+ Cell,
7
+ Tooltip,
8
+ ResponsiveContainer,
9
+ Legend,
10
+ } from 'recharts'
11
+ import type { ModelUsage } from '@/types/claude'
12
+ import { formatTokens } from '@/lib/decode'
13
+
14
+ interface Props {
15
+ modelUsage: Record<string, ModelUsage>
16
+ }
17
+
18
+ const MODEL_COLORS = [
19
+ '#d97706',
20
+ '#34d399',
21
+ '#2563eb',
22
+ '#a78bfa',
23
+ '#fbbf24',
24
+ ]
25
+
26
+ function shortModelName(model: string): string {
27
+ if (model.includes('opus-4-6')) return 'Opus 4.6'
28
+ if (model.includes('opus-4-5')) return 'Opus 4.5'
29
+ if (model.includes('sonnet-4-6')) return 'Sonnet 4.6'
30
+ if (model.includes('sonnet-4-5')) return 'Sonnet 4.5'
31
+ if (model.includes('haiku-4-5')) return 'Haiku 4.5'
32
+ // Generic fallback
33
+ const parts = model.split('-')
34
+ return parts.slice(0, 3).join('-')
35
+ }
36
+
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ function CustomTooltip({ active, payload }: any) {
39
+ if (!active || !payload?.length) return null
40
+ const { name, value } = payload[0]
41
+ return (
42
+ <div className="bg-card border border-border rounded px-3 py-2 text-[13px]">
43
+ <p className="text-muted-foreground">{name}</p>
44
+ <p className="text-foreground font-bold">{formatTokens(value)} tokens</p>
45
+ </div>
46
+ )
47
+ }
48
+
49
+ export function ModelBreakdownDonut({ modelUsage }: Props) {
50
+ const data = Object.entries(modelUsage)
51
+ .map(([model, usage]) => ({
52
+ name: shortModelName(model),
53
+ value: (usage.inputTokens ?? 0) + (usage.outputTokens ?? 0) + (usage.cacheReadInputTokens ?? 0) + (usage.cacheCreationInputTokens ?? 0),
54
+ }))
55
+ .filter(d => d.value > 0)
56
+ .sort((a, b) => b.value - a.value)
57
+
58
+ if (data.length === 0) {
59
+ return (
60
+ <div className="flex items-center justify-center h-48 text-muted-foreground text-sm">
61
+ no model data
62
+ </div>
63
+ )
64
+ }
65
+
66
+ return (
67
+ <ResponsiveContainer width="100%" height={220}>
68
+ <PieChart>
69
+ <Pie
70
+ data={data}
71
+ cx="50%"
72
+ cy="45%"
73
+ innerRadius={55}
74
+ outerRadius={85}
75
+ paddingAngle={2}
76
+ dataKey="value"
77
+ strokeWidth={0}
78
+ >
79
+ {data.map((_, i) => (
80
+ <Cell key={i} fill={MODEL_COLORS[i % MODEL_COLORS.length]} />
81
+ ))}
82
+ </Pie>
83
+ <Tooltip content={<CustomTooltip />} />
84
+ <Legend
85
+ iconType="circle"
86
+ iconSize={8}
87
+ wrapperStyle={{ fontSize: 12 }}
88
+ formatter={(value) => (
89
+ <span style={{ color: 'var(--muted-foreground)', fontSize: 12 }}>{value}</span>
90
+ )}
91
+ />
92
+ </PieChart>
93
+ </ResponsiveContainer>
94
+ )
95
+ }