@bahulam/code 0.1.6 → 0.1.7

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 (178) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +6 -6
  3. package/README.md +10 -17
  4. package/package.json +9 -9
  5. package/src/agents/loader.mjs +1 -1
  6. package/src/agents/workflow_loader.mjs +2 -2
  7. package/src/auth/tarang-auth.mjs +3 -2
  8. package/src/config/model-catalog-default.json +4 -3
  9. package/src/config/model-catalog.mjs +2 -0
  10. package/src/context/prose-chunker.mjs +162 -5
  11. package/src/core/attachments.mjs +65 -3
  12. package/src/core/bundled-runtime.mjs +1 -1
  13. package/src/core/headless.mjs +14 -0
  14. package/src/core/jsonl-writer.mjs +3 -3
  15. package/src/core/lint-resolver.mjs +472 -0
  16. package/src/core/local-agent.mjs +15 -4
  17. package/src/core/local-store.mjs +4 -0
  18. package/src/core/output-filter.mjs +21 -25
  19. package/src/core/pricing.mjs +10 -3
  20. package/src/core/stream-client.mjs +9 -4
  21. package/src/core/tool-executor.mjs +105 -29
  22. package/src/daemon/http-dashboard.mjs +400 -0
  23. package/src/daemon/session-core.mjs +1 -0
  24. package/src/local-service/agent-relay.mjs +868 -0
  25. package/src/local-service/approval-bridge.mjs +222 -0
  26. package/src/local-service/browser.mjs +24 -0
  27. package/src/local-service/command.mjs +155 -0
  28. package/src/local-service/file-access.mjs +393 -0
  29. package/src/local-service/machine.mjs +86 -0
  30. package/src/local-service/paths.mjs +29 -0
  31. package/src/local-service/preview-converters.mjs +260 -0
  32. package/src/local-service/server.mjs +2644 -0
  33. package/src/local-service/session-store.mjs +221 -0
  34. package/src/onboarding/preflight.mjs +1 -1
  35. package/src/terminal/analytics.mjs +2 -6
  36. package/src/terminal/ansi.mjs +11 -3
  37. package/src/terminal/main.mjs +37 -11
  38. package/src/terminal/model-catalog-display.mjs +111 -0
  39. package/src/terminal/repl-format.mjs +4 -1
  40. package/src/terminal/repl-model-form.mjs +81 -14
  41. package/src/terminal/repl-render.mjs +35 -5
  42. package/src/terminal/repl-state.mjs +12 -0
  43. package/src/terminal/repl.mjs +231 -53
  44. package/src/terminal/tool-display.mjs +32 -0
  45. package/src/terminal/watch-state.mjs +118 -0
  46. package/src/tools/analyze-image.mjs +158 -0
  47. package/src/tools/bash.mjs +11 -9
  48. package/src/tools/generate-image.mjs +411 -0
  49. package/src/tools/lint.mjs +24 -43
  50. package/src/tools/registry.mjs +4 -0
  51. package/src/ui/icons.mjs +2 -0
  52. package/src/ui/palette.mjs +1 -1
  53. package/src/ui/slash-commands.mjs +4 -6
  54. package/src/ui/tool-card.mjs +7 -3
  55. package/pulse/app/activity/page.tsx +0 -190
  56. package/pulse/app/api/activity/route.ts +0 -138
  57. package/pulse/app/api/benchmark/route.ts +0 -113
  58. package/pulse/app/api/benchmarks/route.ts +0 -195
  59. package/pulse/app/api/costs/route.ts +0 -88
  60. package/pulse/app/api/export/route.ts +0 -77
  61. package/pulse/app/api/history/route.ts +0 -11
  62. package/pulse/app/api/import/route.ts +0 -31
  63. package/pulse/app/api/memory/route.ts +0 -50
  64. package/pulse/app/api/plans/route.ts +0 -9
  65. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  66. package/pulse/app/api/projects/route.ts +0 -121
  67. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  68. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  69. package/pulse/app/api/sessions/route.ts +0 -112
  70. package/pulse/app/api/settings/route.ts +0 -14
  71. package/pulse/app/api/stats/route.ts +0 -143
  72. package/pulse/app/api/todos/route.ts +0 -9
  73. package/pulse/app/api/tools/route.ts +0 -160
  74. package/pulse/app/benchmarks/page.tsx +0 -224
  75. package/pulse/app/costs/page.tsx +0 -179
  76. package/pulse/app/export/page.tsx +0 -465
  77. package/pulse/app/favicon.ico +0 -0
  78. package/pulse/app/globals.css +0 -263
  79. package/pulse/app/help/page.tsx +0 -143
  80. package/pulse/app/history/page.tsx +0 -157
  81. package/pulse/app/layout.tsx +0 -46
  82. package/pulse/app/memory/page.tsx +0 -365
  83. package/pulse/app/overview-client.tsx +0 -393
  84. package/pulse/app/page.tsx +0 -14
  85. package/pulse/app/plans/page.tsx +0 -308
  86. package/pulse/app/projects/[slug]/page.tsx +0 -390
  87. package/pulse/app/projects/page.tsx +0 -110
  88. package/pulse/app/sessions/[id]/page.tsx +0 -243
  89. package/pulse/app/sessions/page.tsx +0 -39
  90. package/pulse/app/settings/page.tsx +0 -188
  91. package/pulse/app/todos/page.tsx +0 -211
  92. package/pulse/app/tools/page.tsx +0 -249
  93. package/pulse/cli.js +0 -164
  94. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  95. package/pulse/components/activity/streak-card.tsx +0 -36
  96. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  97. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  98. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  99. package/pulse/components/costs/model-token-table.tsx +0 -60
  100. package/pulse/components/global-search.tsx +0 -193
  101. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  102. package/pulse/components/layout/bottom-nav.tsx +0 -53
  103. package/pulse/components/layout/client-layout.tsx +0 -31
  104. package/pulse/components/layout/sidebar-context.tsx +0 -50
  105. package/pulse/components/layout/sidebar.tsx +0 -183
  106. package/pulse/components/layout/top-bar.tsx +0 -121
  107. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  108. package/pulse/components/overview/conversation-table.tsx +0 -148
  109. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  110. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  111. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  112. package/pulse/components/overview/stat-card.tsx +0 -102
  113. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  114. package/pulse/components/projects/project-card.tsx +0 -175
  115. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  116. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  117. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  118. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  119. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  120. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  121. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  122. package/pulse/components/sessions/session-badges.tsx +0 -49
  123. package/pulse/components/sessions/session-table.tsx +0 -299
  124. package/pulse/components/theme-provider.tsx +0 -44
  125. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  126. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  127. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  128. package/pulse/components/tools/version-history-table.tsx +0 -32
  129. package/pulse/components/ui/alert.tsx +0 -66
  130. package/pulse/components/ui/badge.tsx +0 -48
  131. package/pulse/components/ui/breadcrumb.tsx +0 -109
  132. package/pulse/components/ui/button.tsx +0 -64
  133. package/pulse/components/ui/calendar.tsx +0 -220
  134. package/pulse/components/ui/card.tsx +0 -92
  135. package/pulse/components/ui/command.tsx +0 -158
  136. package/pulse/components/ui/dialog.tsx +0 -158
  137. package/pulse/components/ui/input.tsx +0 -21
  138. package/pulse/components/ui/popover.tsx +0 -89
  139. package/pulse/components/ui/progress.tsx +0 -31
  140. package/pulse/components/ui/select.tsx +0 -190
  141. package/pulse/components/ui/separator.tsx +0 -28
  142. package/pulse/components/ui/sheet.tsx +0 -143
  143. package/pulse/components/ui/skeleton.tsx +0 -13
  144. package/pulse/components/ui/table.tsx +0 -116
  145. package/pulse/components/ui/tabs.tsx +0 -91
  146. package/pulse/components/ui/tooltip.tsx +0 -57
  147. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  148. package/pulse/components.json +0 -23
  149. package/pulse/eslint.config.mjs +0 -18
  150. package/pulse/lib/bahulam-paths.ts +0 -23
  151. package/pulse/lib/claude-reader.ts +0 -592
  152. package/pulse/lib/decode.ts +0 -129
  153. package/pulse/lib/pricing.ts +0 -102
  154. package/pulse/lib/replay-parser.ts +0 -165
  155. package/pulse/lib/tool-categories.ts +0 -140
  156. package/pulse/lib/utils.ts +0 -6
  157. package/pulse/next-env.d.ts +0 -6
  158. package/pulse/next.config.ts +0 -16
  159. package/pulse/package.json +0 -45
  160. package/pulse/postcss.config.mjs +0 -7
  161. package/pulse/public/activity.png +0 -0
  162. package/pulse/public/cc-lens.png +0 -0
  163. package/pulse/public/command-k.png +0 -0
  164. package/pulse/public/costs.png +0 -0
  165. package/pulse/public/dashboard-dark.png +0 -0
  166. package/pulse/public/dashboard-white.png +0 -0
  167. package/pulse/public/export.png +0 -0
  168. package/pulse/public/file.svg +0 -1
  169. package/pulse/public/globe.svg +0 -1
  170. package/pulse/public/next.svg +0 -1
  171. package/pulse/public/projects.png +0 -0
  172. package/pulse/public/session-chat.png +0 -0
  173. package/pulse/public/todos.png +0 -0
  174. package/pulse/public/tools.png +0 -0
  175. package/pulse/public/vercel.svg +0 -1
  176. package/pulse/public/window.svg +0 -1
  177. package/pulse/tsconfig.json +0 -34
  178. package/pulse/types/claude.ts +0 -294
@@ -1,183 +0,0 @@
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, Zap,
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: '/benchmarks', label: 'Benchmarks', icon: Zap },
28
- { href: '/settings', label: 'Settings', icon: Settings },
29
- { href: '/help', label: 'Help', icon: HelpCircle },
30
- { href: '/export', label: 'Export', icon: Download },
31
- ]
32
-
33
- function NavItem({
34
- href, label, icon: Icon, active, collapsed,
35
- }: {
36
- href: string; label: string; icon: React.ElementType; active: boolean; collapsed: boolean
37
- }) {
38
- const link = (
39
- <Link
40
- href={href}
41
- className={cn(
42
- 'flex items-center gap-2.5 rounded-md text-sm font-medium transition-colors relative',
43
- collapsed ? 'justify-center p-2.5' : 'px-3 py-2.5',
44
- active
45
- ? 'text-sidebar-primary bg-sidebar-accent border-l-2 border-l-sidebar-primary'
46
- : 'text-sidebar-foreground hover:text-sidebar-accent-foreground hover:bg-sidebar-accent/80',
47
- active && collapsed && 'border-l-0',
48
- )}
49
- >
50
- <Icon className={cn('w-4 h-4 shrink-0', active ? 'text-sidebar-primary' : 'text-sidebar-foreground/60')} />
51
- {!collapsed && label}
52
- </Link>
53
- )
54
-
55
- if (collapsed) {
56
- return (
57
- <Tooltip>
58
- <TooltipTrigger asChild>{link}</TooltipTrigger>
59
- <TooltipContent side="right" className="text-xs">{label}</TooltipContent>
60
- </Tooltip>
61
- )
62
- }
63
- return link
64
- }
65
-
66
- function SidebarContents({
67
- collapsed = false,
68
- onNavigate,
69
- }: {
70
- collapsed?: boolean
71
- onNavigate?: () => void
72
- }) {
73
- const pathname = usePathname()
74
- const { theme, toggle: toggleTheme } = useTheme()
75
- const { toggle: toggleCollapsed } = useSidebar()
76
-
77
- return (
78
- <div className="flex flex-col h-full">
79
- {/* Header */}
80
- <div className={cn(
81
- 'border-b border-sidebar-border flex items-center',
82
- collapsed ? 'justify-center px-2 py-4' : 'justify-between px-4 pt-5 pb-4',
83
- )}>
84
- {!collapsed && (
85
- <span
86
- className={cn(
87
- 'inline-block rounded-md px-2.5 py-1.5 text-[12px] leading-snug tracking-[0.06em]',
88
- 'whitespace-nowrap select-none',
89
- /* Light: cyan on soft tint */
90
- 'text-[#0e7490]',
91
- 'bg-linear-to-b from-[#06b6d4]/14 to-[#06b6d4]/6',
92
- 'ring-1 ring-inset ring-[#06b6d4]/28',
93
- 'shadow-[inset_0_1px_0_rgba(255,255,255,0.7),0_1px_3px_rgba(24,24,27,0.08)]',
94
- /* Dark: cyan glow */
95
- 'dark:text-[#22d3ee]',
96
- 'dark:from-[#22d3ee]/18 dark:to-[#22d3ee]/8 dark:ring-[#22d3ee]/40',
97
- 'dark:shadow-[inset_0_1px_0_rgba(255,255,255,0.06),0_0_22px_-8px_rgba(34,211,238,0.45)]',
98
- '[-webkit-text-stroke:0.35px_rgba(14,116,144,0.35)] dark:[-webkit-text-stroke:0.45px_#06b6d4]',
99
- )}
100
- style={{ fontFamily: 'var(--font-press-start)' }}
101
- >
102
- <span
103
- 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)]"
104
- >
105
- Bahulam Pulse
106
- </span>
107
- </span>
108
- )}
109
- <button
110
- onClick={toggleCollapsed}
111
- aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
112
- 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"
113
- >
114
- {collapsed
115
- ? <PanelLeft className="w-4 h-4" />
116
- : <PanelLeftClose className="w-4 h-4" />}
117
- </button>
118
- </div>
119
-
120
- {/* Nav */}
121
- <nav className={cn('flex-1 py-4 space-y-0.5 overflow-y-auto', collapsed ? 'px-1' : 'px-3')}>
122
- <TooltipProvider delayDuration={100}>
123
- {NAV.map(({ href, label, icon }) => (
124
- <div key={href} onClick={onNavigate}>
125
- <NavItem
126
- href={href}
127
- label={label}
128
- icon={icon}
129
- active={pathname === href}
130
- collapsed={collapsed}
131
- />
132
- </div>
133
- ))}
134
- </TooltipProvider>
135
- </nav>
136
-
137
- {/* Footer */}
138
- <div className={cn(
139
- 'border-t border-sidebar-border flex items-center',
140
- collapsed ? 'justify-center px-2 py-3' : 'justify-between px-4 py-3',
141
- )}>
142
- {!collapsed && (
143
- <span className="text-xs text-sidebar-foreground/50">
144
- Bahulam Pulse
145
- </span>
146
- )}
147
- <button
148
- onClick={toggleTheme}
149
- aria-label="Toggle theme"
150
- className="p-1.5 rounded-md text-sidebar-foreground/50 hover:text-sidebar-foreground hover:bg-sidebar-accent transition-colors cursor-pointer"
151
- >
152
- {theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
153
- </button>
154
- </div>
155
- </div>
156
- )
157
- }
158
-
159
- export function Sidebar() {
160
- const { collapsed, mobileOpen, setMobileOpen } = useSidebar()
161
-
162
- return (
163
- <>
164
- {/* Desktop sidebar */}
165
- <aside
166
- className={cn(
167
- 'hidden md:flex fixed left-0 top-0 h-screen flex-col border-r border-sidebar-border bg-sidebar z-40',
168
- 'transition-[width] duration-300 overflow-hidden',
169
- collapsed ? 'w-14' : 'w-56',
170
- )}
171
- >
172
- <SidebarContents collapsed={collapsed} />
173
- </aside>
174
-
175
- {/* Mobile Sheet */}
176
- <Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
177
- <SheetContent side="left" className="w-56 p-0 bg-sidebar border-sidebar-border">
178
- <SidebarContents onNavigate={() => setMobileOpen(false)} />
179
- </SheetContent>
180
- </Sheet>
181
- </>
182
- )
183
- }
@@ -1,121 +0,0 @@
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
- }
@@ -1,107 +0,0 @@
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
- }
@@ -1,148 +0,0 @@
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
- }
@@ -1,95 +0,0 @@
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
- }