@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.
- package/README.md +82 -0
- package/package.json +36 -4
- package/pulse/app/activity/page.tsx +190 -0
- package/pulse/app/api/activity/route.ts +138 -0
- package/pulse/app/api/costs/route.ts +88 -0
- package/pulse/app/api/export/route.ts +77 -0
- package/pulse/app/api/history/route.ts +11 -0
- package/pulse/app/api/import/route.ts +31 -0
- package/pulse/app/api/memory/route.ts +52 -0
- package/pulse/app/api/plans/route.ts +9 -0
- package/pulse/app/api/projects/[slug]/route.ts +96 -0
- package/pulse/app/api/projects/route.ts +121 -0
- package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
- package/pulse/app/api/sessions/[id]/route.ts +31 -0
- package/pulse/app/api/sessions/route.ts +112 -0
- package/pulse/app/api/settings/route.ts +14 -0
- package/pulse/app/api/stats/route.ts +143 -0
- package/pulse/app/api/todos/route.ts +9 -0
- package/pulse/app/api/tools/route.ts +160 -0
- package/pulse/app/costs/page.tsx +179 -0
- package/pulse/app/export/page.tsx +465 -0
- package/pulse/app/favicon.ico +0 -0
- package/pulse/app/globals.css +263 -0
- package/pulse/app/help/page.tsx +142 -0
- package/pulse/app/history/page.tsx +157 -0
- package/pulse/app/layout.tsx +46 -0
- package/pulse/app/memory/page.tsx +365 -0
- package/pulse/app/overview-client.tsx +393 -0
- package/pulse/app/page.tsx +14 -0
- package/pulse/app/plans/page.tsx +308 -0
- package/pulse/app/projects/[slug]/page.tsx +390 -0
- package/pulse/app/projects/page.tsx +110 -0
- package/pulse/app/sessions/[id]/page.tsx +243 -0
- package/pulse/app/sessions/page.tsx +39 -0
- package/pulse/app/settings/page.tsx +188 -0
- package/pulse/app/todos/page.tsx +211 -0
- package/pulse/app/tools/page.tsx +249 -0
- package/pulse/cli.js +159 -0
- package/pulse/components/activity/day-of-week-chart.tsx +35 -0
- package/pulse/components/activity/streak-card.tsx +36 -0
- package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
- package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
- package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
- package/pulse/components/costs/model-token-table.tsx +60 -0
- package/pulse/components/global-search.tsx +193 -0
- package/pulse/components/keyboard-nav-provider.tsx +23 -0
- package/pulse/components/layout/bottom-nav.tsx +52 -0
- package/pulse/components/layout/client-layout.tsx +31 -0
- package/pulse/components/layout/sidebar-context.tsx +50 -0
- package/pulse/components/layout/sidebar.tsx +182 -0
- package/pulse/components/layout/top-bar.tsx +121 -0
- package/pulse/components/overview/activity-heatmap.tsx +107 -0
- package/pulse/components/overview/conversation-table.tsx +148 -0
- package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
- package/pulse/components/overview/peak-hours-chart.tsx +87 -0
- package/pulse/components/overview/project-activity-donut.tsx +96 -0
- package/pulse/components/overview/stat-card.tsx +102 -0
- package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
- package/pulse/components/projects/project-card.tsx +175 -0
- package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
- package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
- package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
- package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
- package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
- package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
- package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
- package/pulse/components/sessions/session-badges.tsx +49 -0
- package/pulse/components/sessions/session-table.tsx +299 -0
- package/pulse/components/theme-provider.tsx +44 -0
- package/pulse/components/tools/feature-adoption-table.tsx +58 -0
- package/pulse/components/tools/mcp-server-panel.tsx +45 -0
- package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
- package/pulse/components/tools/version-history-table.tsx +32 -0
- package/pulse/components/ui/alert.tsx +66 -0
- package/pulse/components/ui/badge.tsx +48 -0
- package/pulse/components/ui/breadcrumb.tsx +109 -0
- package/pulse/components/ui/button.tsx +64 -0
- package/pulse/components/ui/calendar.tsx +220 -0
- package/pulse/components/ui/card.tsx +92 -0
- package/pulse/components/ui/command.tsx +158 -0
- package/pulse/components/ui/dialog.tsx +158 -0
- package/pulse/components/ui/input.tsx +21 -0
- package/pulse/components/ui/popover.tsx +89 -0
- package/pulse/components/ui/progress.tsx +31 -0
- package/pulse/components/ui/select.tsx +190 -0
- package/pulse/components/ui/separator.tsx +28 -0
- package/pulse/components/ui/sheet.tsx +143 -0
- package/pulse/components/ui/skeleton.tsx +13 -0
- package/pulse/components/ui/table.tsx +116 -0
- package/pulse/components/ui/tabs.tsx +91 -0
- package/pulse/components/ui/tooltip.tsx +57 -0
- package/pulse/components/use-global-keyboard-nav.ts +79 -0
- package/pulse/components.json +23 -0
- package/pulse/eslint.config.mjs +18 -0
- package/pulse/lib/claude-reader.ts +594 -0
- package/pulse/lib/decode.ts +129 -0
- package/pulse/lib/pricing.ts +102 -0
- package/pulse/lib/replay-parser.ts +165 -0
- package/pulse/lib/tool-categories.ts +127 -0
- package/pulse/lib/utils.ts +6 -0
- package/pulse/next-env.d.ts +6 -0
- package/pulse/next.config.ts +16 -0
- package/pulse/package.json +45 -0
- package/pulse/postcss.config.mjs +7 -0
- package/pulse/public/activity.png +0 -0
- package/pulse/public/cc-lens.png +0 -0
- package/pulse/public/command-k.png +0 -0
- package/pulse/public/costs.png +0 -0
- package/pulse/public/dashboard-dark.png +0 -0
- package/pulse/public/dashboard-white.png +0 -0
- package/pulse/public/export.png +0 -0
- package/pulse/public/file.svg +1 -0
- package/pulse/public/globe.svg +1 -0
- package/pulse/public/next.svg +1 -0
- package/pulse/public/projects.png +0 -0
- package/pulse/public/session-chat.png +0 -0
- package/pulse/public/todos.png +0 -0
- package/pulse/public/tools.png +0 -0
- package/pulse/public/vercel.svg +1 -0
- package/pulse/public/window.svg +1 -0
- package/pulse/tsconfig.json +34 -0
- package/pulse/types/claude.ts +294 -0
- package/src/agents/loader.mjs +89 -0
- package/src/agents/parser.mjs +98 -0
- package/src/agents/teams.mjs +123 -0
- package/src/auth/oauth.mjs +220 -0
- package/src/auth/tarang-auth.mjs +277 -0
- package/src/config/cli-args.mjs +173 -0
- package/src/config/env.mjs +263 -0
- package/src/config/settings.mjs +132 -0
- package/src/context/ast-parser.mjs +298 -0
- package/src/context/bm25.mjs +85 -0
- package/src/context/retriever.mjs +270 -0
- package/src/context/skeleton.mjs +134 -0
- package/src/core/agent-loop.mjs +480 -0
- package/src/core/approval.mjs +273 -0
- package/src/core/backend-url.mjs +57 -0
- package/src/core/cache.mjs +105 -0
- package/src/core/callback-client.mjs +149 -0
- package/src/core/checkpoints.mjs +142 -0
- package/src/core/context-manager.mjs +198 -0
- package/src/core/headless.mjs +168 -0
- package/src/core/hooks-manager.mjs +87 -0
- package/src/core/jsonl-writer.mjs +351 -0
- package/src/core/local-agent.mjs +429 -0
- package/src/core/local-store.mjs +325 -0
- package/src/core/mode-selector.mjs +51 -0
- package/src/core/output-filter.mjs +177 -0
- package/src/core/paths.mjs +98 -0
- package/src/core/pricing.mjs +314 -0
- package/src/core/providers.mjs +219 -0
- package/src/core/rate-limiter.mjs +119 -0
- package/src/core/safety.mjs +200 -0
- package/src/core/scheduler.mjs +173 -0
- package/src/core/session-manager.mjs +317 -0
- package/src/core/session.mjs +143 -0
- package/src/core/settings-sync.mjs +85 -0
- package/src/core/stagnation.mjs +57 -0
- package/src/core/stream-client.mjs +367 -0
- package/src/core/streaming.mjs +182 -0
- package/src/core/system-prompt.mjs +135 -0
- package/src/core/tool-executor.mjs +725 -0
- package/src/hooks/engine.mjs +162 -0
- package/src/index.mjs +370 -0
- package/src/mcp/client.mjs +253 -0
- package/src/mcp/transport-shttp.mjs +130 -0
- package/src/mcp/transport-sse.mjs +131 -0
- package/src/mcp/transport-ws.mjs +134 -0
- package/src/permissions/checker.mjs +57 -0
- package/src/permissions/command-classifier.mjs +573 -0
- package/src/permissions/injection-check.mjs +60 -0
- package/src/permissions/path-check.mjs +102 -0
- package/src/permissions/prompt.mjs +73 -0
- package/src/permissions/sandbox.mjs +112 -0
- package/src/plugins/loader.mjs +138 -0
- package/src/skills/loader.mjs +147 -0
- package/src/skills/runner.mjs +55 -0
- package/src/telemetry/index.mjs +96 -0
- package/src/terminal/agents.mjs +177 -0
- package/src/terminal/analytics.mjs +292 -0
- package/src/terminal/ansi.mjs +421 -0
- package/src/terminal/main.mjs +150 -0
- package/src/terminal/repl.mjs +1484 -0
- package/src/terminal/tool-display.mjs +58 -0
- package/src/tools/agent.mjs +137 -0
- package/src/tools/ask-user.mjs +61 -0
- package/src/tools/bash.mjs +148 -0
- package/src/tools/cron-create.mjs +120 -0
- package/src/tools/cron-delete.mjs +49 -0
- package/src/tools/cron-list.mjs +37 -0
- package/src/tools/edit.mjs +82 -0
- package/src/tools/enter-worktree.mjs +69 -0
- package/src/tools/exit-worktree.mjs +57 -0
- package/src/tools/glob.mjs +117 -0
- package/src/tools/grep.mjs +129 -0
- package/src/tools/lint.mjs +71 -0
- package/src/tools/ls.mjs +58 -0
- package/src/tools/lsp.mjs +115 -0
- package/src/tools/multi-edit.mjs +94 -0
- package/src/tools/notebook-edit.mjs +96 -0
- package/src/tools/read-mcp-resource.mjs +57 -0
- package/src/tools/read.mjs +138 -0
- package/src/tools/registry.mjs +132 -0
- package/src/tools/remote-trigger.mjs +84 -0
- package/src/tools/send-message.mjs +64 -0
- package/src/tools/skill.mjs +52 -0
- package/src/tools/test-runner.mjs +49 -0
- package/src/tools/todo-write.mjs +68 -0
- package/src/tools/tool-search.mjs +77 -0
- package/src/tools/web-fetch.mjs +65 -0
- package/src/tools/web-search.mjs +89 -0
- package/src/tools/write.mjs +55 -0
- package/src/ui/banner.mjs +237 -0
- package/src/ui/commands.mjs +499 -0
- package/src/ui/formatter.mjs +379 -0
- package/src/ui/markdown.mjs +278 -0
- package/src/ui/slash-commands.mjs +258 -0
- 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
|
+
}
|