@axplusb/kepler 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +82 -0
  2. package/package.json +36 -4
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/costs/route.ts +88 -0
  6. package/pulse/app/api/export/route.ts +77 -0
  7. package/pulse/app/api/history/route.ts +11 -0
  8. package/pulse/app/api/import/route.ts +31 -0
  9. package/pulse/app/api/memory/route.ts +52 -0
  10. package/pulse/app/api/plans/route.ts +9 -0
  11. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  12. package/pulse/app/api/projects/route.ts +121 -0
  13. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  14. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  15. package/pulse/app/api/sessions/route.ts +112 -0
  16. package/pulse/app/api/settings/route.ts +14 -0
  17. package/pulse/app/api/stats/route.ts +143 -0
  18. package/pulse/app/api/todos/route.ts +9 -0
  19. package/pulse/app/api/tools/route.ts +160 -0
  20. package/pulse/app/costs/page.tsx +179 -0
  21. package/pulse/app/export/page.tsx +465 -0
  22. package/pulse/app/favicon.ico +0 -0
  23. package/pulse/app/globals.css +263 -0
  24. package/pulse/app/help/page.tsx +142 -0
  25. package/pulse/app/history/page.tsx +157 -0
  26. package/pulse/app/layout.tsx +46 -0
  27. package/pulse/app/memory/page.tsx +365 -0
  28. package/pulse/app/overview-client.tsx +393 -0
  29. package/pulse/app/page.tsx +14 -0
  30. package/pulse/app/plans/page.tsx +308 -0
  31. package/pulse/app/projects/[slug]/page.tsx +390 -0
  32. package/pulse/app/projects/page.tsx +110 -0
  33. package/pulse/app/sessions/[id]/page.tsx +243 -0
  34. package/pulse/app/sessions/page.tsx +39 -0
  35. package/pulse/app/settings/page.tsx +188 -0
  36. package/pulse/app/todos/page.tsx +211 -0
  37. package/pulse/app/tools/page.tsx +249 -0
  38. package/pulse/cli.js +159 -0
  39. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  40. package/pulse/components/activity/streak-card.tsx +36 -0
  41. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  42. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  43. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  44. package/pulse/components/costs/model-token-table.tsx +60 -0
  45. package/pulse/components/global-search.tsx +193 -0
  46. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  47. package/pulse/components/layout/bottom-nav.tsx +52 -0
  48. package/pulse/components/layout/client-layout.tsx +31 -0
  49. package/pulse/components/layout/sidebar-context.tsx +50 -0
  50. package/pulse/components/layout/sidebar.tsx +182 -0
  51. package/pulse/components/layout/top-bar.tsx +121 -0
  52. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  53. package/pulse/components/overview/conversation-table.tsx +148 -0
  54. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  55. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  56. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  57. package/pulse/components/overview/stat-card.tsx +102 -0
  58. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  59. package/pulse/components/projects/project-card.tsx +175 -0
  60. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  61. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  62. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  63. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  64. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  65. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  66. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  67. package/pulse/components/sessions/session-badges.tsx +49 -0
  68. package/pulse/components/sessions/session-table.tsx +299 -0
  69. package/pulse/components/theme-provider.tsx +44 -0
  70. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  71. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  72. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  73. package/pulse/components/tools/version-history-table.tsx +32 -0
  74. package/pulse/components/ui/alert.tsx +66 -0
  75. package/pulse/components/ui/badge.tsx +48 -0
  76. package/pulse/components/ui/breadcrumb.tsx +109 -0
  77. package/pulse/components/ui/button.tsx +64 -0
  78. package/pulse/components/ui/calendar.tsx +220 -0
  79. package/pulse/components/ui/card.tsx +92 -0
  80. package/pulse/components/ui/command.tsx +158 -0
  81. package/pulse/components/ui/dialog.tsx +158 -0
  82. package/pulse/components/ui/input.tsx +21 -0
  83. package/pulse/components/ui/popover.tsx +89 -0
  84. package/pulse/components/ui/progress.tsx +31 -0
  85. package/pulse/components/ui/select.tsx +190 -0
  86. package/pulse/components/ui/separator.tsx +28 -0
  87. package/pulse/components/ui/sheet.tsx +143 -0
  88. package/pulse/components/ui/skeleton.tsx +13 -0
  89. package/pulse/components/ui/table.tsx +116 -0
  90. package/pulse/components/ui/tabs.tsx +91 -0
  91. package/pulse/components/ui/tooltip.tsx +57 -0
  92. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  93. package/pulse/components.json +23 -0
  94. package/pulse/eslint.config.mjs +18 -0
  95. package/pulse/lib/claude-reader.ts +594 -0
  96. package/pulse/lib/decode.ts +129 -0
  97. package/pulse/lib/pricing.ts +102 -0
  98. package/pulse/lib/replay-parser.ts +165 -0
  99. package/pulse/lib/tool-categories.ts +127 -0
  100. package/pulse/lib/utils.ts +6 -0
  101. package/pulse/next-env.d.ts +6 -0
  102. package/pulse/next.config.ts +16 -0
  103. package/pulse/package.json +45 -0
  104. package/pulse/postcss.config.mjs +7 -0
  105. package/pulse/public/activity.png +0 -0
  106. package/pulse/public/cc-lens.png +0 -0
  107. package/pulse/public/command-k.png +0 -0
  108. package/pulse/public/costs.png +0 -0
  109. package/pulse/public/dashboard-dark.png +0 -0
  110. package/pulse/public/dashboard-white.png +0 -0
  111. package/pulse/public/export.png +0 -0
  112. package/pulse/public/file.svg +1 -0
  113. package/pulse/public/globe.svg +1 -0
  114. package/pulse/public/next.svg +1 -0
  115. package/pulse/public/projects.png +0 -0
  116. package/pulse/public/session-chat.png +0 -0
  117. package/pulse/public/todos.png +0 -0
  118. package/pulse/public/tools.png +0 -0
  119. package/pulse/public/vercel.svg +1 -0
  120. package/pulse/public/window.svg +1 -0
  121. package/pulse/tsconfig.json +34 -0
  122. package/pulse/types/claude.ts +294 -0
  123. package/src/agents/loader.mjs +89 -0
  124. package/src/agents/parser.mjs +98 -0
  125. package/src/agents/teams.mjs +123 -0
  126. package/src/auth/oauth.mjs +220 -0
  127. package/src/auth/tarang-auth.mjs +277 -0
  128. package/src/config/cli-args.mjs +173 -0
  129. package/src/config/env.mjs +263 -0
  130. package/src/config/settings.mjs +132 -0
  131. package/src/context/ast-parser.mjs +298 -0
  132. package/src/context/bm25.mjs +85 -0
  133. package/src/context/retriever.mjs +270 -0
  134. package/src/context/skeleton.mjs +134 -0
  135. package/src/core/agent-loop.mjs +480 -0
  136. package/src/core/approval.mjs +273 -0
  137. package/src/core/backend-url.mjs +57 -0
  138. package/src/core/cache.mjs +105 -0
  139. package/src/core/callback-client.mjs +149 -0
  140. package/src/core/checkpoints.mjs +142 -0
  141. package/src/core/context-manager.mjs +198 -0
  142. package/src/core/headless.mjs +168 -0
  143. package/src/core/hooks-manager.mjs +87 -0
  144. package/src/core/jsonl-writer.mjs +351 -0
  145. package/src/core/local-agent.mjs +429 -0
  146. package/src/core/local-store.mjs +325 -0
  147. package/src/core/mode-selector.mjs +51 -0
  148. package/src/core/output-filter.mjs +177 -0
  149. package/src/core/paths.mjs +98 -0
  150. package/src/core/pricing.mjs +314 -0
  151. package/src/core/providers.mjs +219 -0
  152. package/src/core/rate-limiter.mjs +119 -0
  153. package/src/core/safety.mjs +200 -0
  154. package/src/core/scheduler.mjs +173 -0
  155. package/src/core/session-manager.mjs +317 -0
  156. package/src/core/session.mjs +143 -0
  157. package/src/core/settings-sync.mjs +85 -0
  158. package/src/core/stagnation.mjs +57 -0
  159. package/src/core/stream-client.mjs +367 -0
  160. package/src/core/streaming.mjs +182 -0
  161. package/src/core/system-prompt.mjs +135 -0
  162. package/src/core/tool-executor.mjs +725 -0
  163. package/src/hooks/engine.mjs +162 -0
  164. package/src/index.mjs +370 -0
  165. package/src/mcp/client.mjs +253 -0
  166. package/src/mcp/transport-shttp.mjs +130 -0
  167. package/src/mcp/transport-sse.mjs +131 -0
  168. package/src/mcp/transport-ws.mjs +134 -0
  169. package/src/permissions/checker.mjs +57 -0
  170. package/src/permissions/command-classifier.mjs +573 -0
  171. package/src/permissions/injection-check.mjs +60 -0
  172. package/src/permissions/path-check.mjs +102 -0
  173. package/src/permissions/prompt.mjs +73 -0
  174. package/src/permissions/sandbox.mjs +112 -0
  175. package/src/plugins/loader.mjs +138 -0
  176. package/src/skills/loader.mjs +147 -0
  177. package/src/skills/runner.mjs +55 -0
  178. package/src/telemetry/index.mjs +96 -0
  179. package/src/terminal/agents.mjs +177 -0
  180. package/src/terminal/analytics.mjs +292 -0
  181. package/src/terminal/ansi.mjs +421 -0
  182. package/src/terminal/main.mjs +150 -0
  183. package/src/terminal/repl.mjs +1484 -0
  184. package/src/terminal/tool-display.mjs +58 -0
  185. package/src/tools/agent.mjs +137 -0
  186. package/src/tools/ask-user.mjs +61 -0
  187. package/src/tools/bash.mjs +148 -0
  188. package/src/tools/cron-create.mjs +120 -0
  189. package/src/tools/cron-delete.mjs +49 -0
  190. package/src/tools/cron-list.mjs +37 -0
  191. package/src/tools/edit.mjs +82 -0
  192. package/src/tools/enter-worktree.mjs +69 -0
  193. package/src/tools/exit-worktree.mjs +57 -0
  194. package/src/tools/glob.mjs +117 -0
  195. package/src/tools/grep.mjs +129 -0
  196. package/src/tools/lint.mjs +71 -0
  197. package/src/tools/ls.mjs +58 -0
  198. package/src/tools/lsp.mjs +115 -0
  199. package/src/tools/multi-edit.mjs +94 -0
  200. package/src/tools/notebook-edit.mjs +96 -0
  201. package/src/tools/read-mcp-resource.mjs +57 -0
  202. package/src/tools/read.mjs +138 -0
  203. package/src/tools/registry.mjs +132 -0
  204. package/src/tools/remote-trigger.mjs +84 -0
  205. package/src/tools/send-message.mjs +64 -0
  206. package/src/tools/skill.mjs +52 -0
  207. package/src/tools/test-runner.mjs +49 -0
  208. package/src/tools/todo-write.mjs +68 -0
  209. package/src/tools/tool-search.mjs +77 -0
  210. package/src/tools/web-fetch.mjs +65 -0
  211. package/src/tools/web-search.mjs +89 -0
  212. package/src/tools/write.mjs +55 -0
  213. package/src/ui/banner.mjs +237 -0
  214. package/src/ui/commands.mjs +499 -0
  215. package/src/ui/formatter.mjs +379 -0
  216. package/src/ui/markdown.mjs +278 -0
  217. package/src/ui/slash-commands.mjs +258 -0
  218. package/index.js +0 -1
@@ -0,0 +1,95 @@
1
+ 'use client'
2
+
3
+ import { useMemo, useState } from 'react'
4
+ import { AreaChart, Area, XAxis, YAxis, Tooltip, CartesianGrid, ResponsiveContainer, Legend } from 'recharts'
5
+ import { formatCost } from '@/lib/decode'
6
+ import type { DailyCost } from '@/types/claude'
7
+
8
+ const MODEL_COLORS: Record<string, string> = {
9
+ 'claude-opus-4-6': '#d97706',
10
+ 'claude-opus-4-5-20251101': '#a78bfa',
11
+ 'claude-sonnet-4-6': 'var(--viz-sky)',
12
+ 'claude-haiku-4-5': '#34d399',
13
+ }
14
+
15
+ function colorForModel(m: string): string {
16
+ for (const [key, col] of Object.entries(MODEL_COLORS)) {
17
+ if (m.includes(key.split('-').slice(2).join('-'))) return col
18
+ }
19
+ return '#7a8494'
20
+ }
21
+
22
+ function shortModel(m: string): string {
23
+ if (m.includes('opus-4-6')) return 'Opus 4.6'
24
+ if (m.includes('opus-4-5')) return 'Opus 4.5'
25
+ if (m.includes('sonnet-4-6')) return 'Sonnet 4.6'
26
+ if (m.includes('haiku-4-5')) return 'Haiku 4.5'
27
+ return m
28
+ }
29
+
30
+ interface Props {
31
+ daily: DailyCost[]
32
+ }
33
+
34
+ type Window = 30 | 90 | 365
35
+
36
+ export function CostOverTimeChart({ daily }: Props) {
37
+ const [window, setWindow] = useState<Window>(90)
38
+
39
+ const { data, models } = useMemo(() => {
40
+ const sorted = [...daily].sort((a, b) => a.date.localeCompare(b.date))
41
+ const sliced = sorted.slice(-window)
42
+ const modelSet = new Set<string>()
43
+ for (const d of sliced) Object.keys(d.costs ?? {}).forEach(m => modelSet.add(m))
44
+ const models = [...modelSet]
45
+ return {
46
+ data: sliced.map(d => ({
47
+ date: d.date.slice(5), // MM-DD
48
+ ...Object.fromEntries(models.map(m => [m, d.costs[m] ?? 0])),
49
+ total: d.total,
50
+ })),
51
+ models,
52
+ }
53
+ }, [daily, window])
54
+
55
+ return (
56
+ <div>
57
+ <div className="flex items-center justify-between mb-3">
58
+ <h3 className="text-[13px] font-bold text-muted-foreground uppercase tracking-widest">Cost Over Time</h3>
59
+ <div className="flex gap-1">
60
+ {([30, 90, 365] as Window[]).map(w => (
61
+ <button
62
+ key={w}
63
+ onClick={() => setWindow(w)}
64
+ className={`px-2 py-0.5 rounded text-[12px] transition-colors ${window === w ? 'bg-primary text-black font-bold' : 'text-muted-foreground hover:text-foreground border border-border'}`}
65
+ >
66
+ {w === 365 ? 'All' : `${w}d`}
67
+ </button>
68
+ ))}
69
+ </div>
70
+ </div>
71
+ <ResponsiveContainer width="100%" height={200}>
72
+ <AreaChart data={data} margin={{ top: 4, right: 8, bottom: 0, left: 0 }}>
73
+ <CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
74
+ <XAxis dataKey="date" tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }} tickLine={false} axisLine={false} interval="preserveStartEnd" />
75
+ <YAxis tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }} tickLine={false} axisLine={false} tickFormatter={v => `$${v.toFixed(2)}`} width={48} />
76
+ <Tooltip
77
+ contentStyle={{ background: 'var(--card)', border: '1px solid var(--border)', borderRadius: 4, fontSize: 12 }}
78
+ formatter={(val: number | undefined, name?: string) => [formatCost(val ?? 0), shortModel(name ?? '')]}
79
+ />
80
+ {models.map(m => (
81
+ <Area
82
+ key={m}
83
+ type="monotone"
84
+ dataKey={m}
85
+ stackId="1"
86
+ stroke={colorForModel(m)}
87
+ fill={colorForModel(m) + '30'}
88
+ strokeWidth={1.5}
89
+ />
90
+ ))}
91
+ </AreaChart>
92
+ </ResponsiveContainer>
93
+ </div>
94
+ )
95
+ }
@@ -0,0 +1,60 @@
1
+ import { formatTokens, formatCost } from '@/lib/decode'
2
+ import type { ModelCostBreakdown } from '@/types/claude'
3
+
4
+ function shortModel(m: string): string {
5
+ if (m.includes('opus-4-6')) return 'claude-opus-4.6'
6
+ if (m.includes('opus-4-5')) return 'claude-opus-4.5'
7
+ if (m.includes('sonnet-4-6')) return 'claude-sonnet-4.6'
8
+ if (m.includes('haiku-4-5')) return 'claude-haiku-4.5'
9
+ return m
10
+ }
11
+
12
+ interface Props {
13
+ models: ModelCostBreakdown[]
14
+ }
15
+
16
+ export function ModelTokenTable({ models }: Props) {
17
+ const totals = models.reduce((acc, m) => ({
18
+ input: acc.input + m.input_tokens,
19
+ output: acc.output + m.output_tokens,
20
+ cacheWrite: acc.cacheWrite + m.cache_write_tokens,
21
+ cacheRead: acc.cacheRead + m.cache_read_tokens,
22
+ cost: acc.cost + m.estimated_cost,
23
+ }), { input: 0, output: 0, cacheWrite: 0, cacheRead: 0, cost: 0 })
24
+
25
+ return (
26
+ <div className="overflow-x-auto">
27
+ <table className="w-full text-[13px] font-mono">
28
+ <thead>
29
+ <tr className="border-b border-border">
30
+ {['Model', 'Input', 'Output', 'Cache W', 'Cache R', 'Cost'].map(h => (
31
+ <th key={h} className={`py-2 text-[12px] font-bold text-muted-foreground uppercase tracking-wider ${h === 'Model' ? 'text-left' : 'text-right'}`}>
32
+ {h}
33
+ </th>
34
+ ))}
35
+ </tr>
36
+ </thead>
37
+ <tbody>
38
+ {models.map(m => (
39
+ <tr key={m.model} className="border-b border-border/30 hover:bg-muted/50 transition-colors">
40
+ <td className="py-2 text-foreground/80">{shortModel(m.model)}</td>
41
+ <td className="py-2 text-right text-blue-700 dark:text-[#60a5fa]">{formatTokens(m.input_tokens)}</td>
42
+ <td className="py-2 text-right text-[#d97706]">{formatTokens(m.output_tokens)}</td>
43
+ <td className="py-2 text-right text-[#a78bfa]">{formatTokens(m.cache_write_tokens)}</td>
44
+ <td className="py-2 text-right text-[#34d399]">{formatTokens(m.cache_read_tokens)}</td>
45
+ <td className="py-2 text-right text-[#d97706] font-bold">{formatCost(m.estimated_cost)}</td>
46
+ </tr>
47
+ ))}
48
+ <tr className="border-t border-border font-bold">
49
+ <td className="py-2 text-muted-foreground">TOTAL</td>
50
+ <td className="py-2 text-right text-blue-700 dark:text-[#60a5fa]">{formatTokens(totals.input)}</td>
51
+ <td className="py-2 text-right text-[#d97706]">{formatTokens(totals.output)}</td>
52
+ <td className="py-2 text-right text-[#a78bfa]">{formatTokens(totals.cacheWrite)}</td>
53
+ <td className="py-2 text-right text-[#34d399]">{formatTokens(totals.cacheRead)}</td>
54
+ <td className="py-2 text-right text-[#d97706]">{formatCost(totals.cost)}</td>
55
+ </tr>
56
+ </tbody>
57
+ </table>
58
+ </div>
59
+ )
60
+ }
@@ -0,0 +1,193 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect } from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+ import { LayoutDashboard, FileText, FolderOpen, Layers, Brain } from 'lucide-react'
6
+ import {
7
+ CommandDialog,
8
+ CommandEmpty,
9
+ CommandGroup,
10
+ CommandInput,
11
+ CommandItem,
12
+ CommandList,
13
+ CommandSeparator,
14
+ CommandShortcut,
15
+ } from '@/components/ui/command'
16
+ import { projectDisplayName } from '@/lib/decode'
17
+
18
+ const PAGES = [
19
+ { label: 'Overview', href: '/' },
20
+ { label: 'Projects', href: '/projects' },
21
+ { label: 'Sessions', href: '/sessions' },
22
+ { label: 'Costs', href: '/costs' },
23
+ { label: 'Tools', href: '/tools' },
24
+ { label: 'Activity', href: '/activity' },
25
+ { label: 'History', href: '/history' },
26
+ { label: 'Todos', href: '/todos' },
27
+ { label: 'Plans', href: '/plans' },
28
+ { label: 'Memory', href: '/memory' },
29
+ { label: 'Settings', href: '/settings' },
30
+ { label: 'Export', href: '/export' },
31
+ ]
32
+
33
+ export function GlobalSearch() {
34
+ const [open, setOpen] = useState(false)
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ const [sessions, setSessions] = useState<any[]>([])
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ const [projects, setProjects] = useState<any[]>([])
39
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
40
+ const [plans, setPlans] = useState<any[]>([])
41
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
+ const [memories, setMemories] = useState<any[]>([])
43
+ const [loaded, setLoaded] = useState(false)
44
+ const router = useRouter()
45
+
46
+ // Lazy-load data on first open
47
+ useEffect(() => {
48
+ if (!open || loaded) return
49
+ Promise.all([
50
+ fetch('/api/sessions').then(r => r.json()),
51
+ fetch('/api/projects').then(r => r.json()),
52
+ fetch('/api/plans').then(r => r.json()),
53
+ fetch('/api/memory').then(r => r.json()),
54
+ ]).then(([s, p, pl, m]) => {
55
+ setSessions(s.sessions ?? [])
56
+ setProjects(p.projects ?? [])
57
+ setPlans(pl.plans ?? [])
58
+ setMemories(m.memories ?? [])
59
+ setLoaded(true)
60
+ }).catch(() => setLoaded(true))
61
+ }, [open, loaded])
62
+
63
+ // Listen for open-search custom event (fired by TopBar button)
64
+ useEffect(() => {
65
+ const handler = () => setOpen(true)
66
+ window.addEventListener('open-search', handler)
67
+ return () => window.removeEventListener('open-search', handler)
68
+ }, [])
69
+
70
+ // Cmd+K / Ctrl+K shortcut
71
+ useEffect(() => {
72
+ const handler = (e: KeyboardEvent) => {
73
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
74
+ e.preventDefault()
75
+ setOpen(o => !o)
76
+ }
77
+ }
78
+ window.addEventListener('keydown', handler)
79
+ return () => window.removeEventListener('keydown', handler)
80
+ }, [])
81
+
82
+ function navigate(href: string) {
83
+ router.push(href)
84
+ setOpen(false)
85
+ }
86
+
87
+ return (
88
+ <CommandDialog open={open} onOpenChange={setOpen}>
89
+ <CommandInput placeholder="Search pages, sessions, projects..." />
90
+ <CommandList>
91
+ <CommandEmpty>No results found.</CommandEmpty>
92
+
93
+ <CommandGroup heading="Pages">
94
+ {PAGES.map(p => (
95
+ <CommandItem key={p.href} value={p.label} onSelect={() => navigate(p.href)}>
96
+ <LayoutDashboard className="w-3.5 h-3.5 text-muted-foreground shrink-0" />
97
+ {p.label}
98
+ <CommandShortcut>page</CommandShortcut>
99
+ </CommandItem>
100
+ ))}
101
+ </CommandGroup>
102
+
103
+ {sessions.length > 0 && (
104
+ <>
105
+ <CommandSeparator />
106
+ <CommandGroup heading="Sessions">
107
+ {sessions.slice(0, 80).map(s => (
108
+ <CommandItem
109
+ key={s.session_id}
110
+ value={`${s.slug ?? ''} ${s.first_prompt ?? ''} ${projectDisplayName(s.project_path ?? '')}`}
111
+ onSelect={() => navigate(`/sessions/${s.session_id}`)}
112
+ >
113
+ <FileText className="w-3.5 h-3.5 text-blue-700 dark:text-blue-400 shrink-0" />
114
+ <div className="flex-1 min-w-0">
115
+ <span className="text-sm">{s.slug ?? s.session_id.slice(0, 12)}</span>
116
+ {s.first_prompt && (
117
+ <p className="text-xs text-muted-foreground truncate">{s.first_prompt.slice(0, 60)}</p>
118
+ )}
119
+ </div>
120
+ <CommandShortcut>session</CommandShortcut>
121
+ </CommandItem>
122
+ ))}
123
+ </CommandGroup>
124
+ </>
125
+ )}
126
+
127
+ {projects.length > 0 && (
128
+ <>
129
+ <CommandSeparator />
130
+ <CommandGroup heading="Projects">
131
+ {projects.map(p => (
132
+ <CommandItem
133
+ key={p.slug}
134
+ value={`${p.display_name} ${p.slug}`}
135
+ onSelect={() => navigate(`/projects/${p.slug}`)}
136
+ >
137
+ <FolderOpen className="w-3.5 h-3.5 text-green-400 shrink-0" />
138
+ <span className="flex-1 truncate">{p.display_name}</span>
139
+ <CommandShortcut>{p.session_count} sessions</CommandShortcut>
140
+ </CommandItem>
141
+ ))}
142
+ </CommandGroup>
143
+ </>
144
+ )}
145
+
146
+ {plans.length > 0 && (
147
+ <>
148
+ <CommandSeparator />
149
+ <CommandGroup heading="Plans">
150
+ {plans.map((p, i) => (
151
+ <CommandItem
152
+ key={`plan-${i}`}
153
+ value={`${p.name} ${(p.content ?? '').slice(0, 200)}`}
154
+ onSelect={() => navigate('/plans')}
155
+ >
156
+ <Layers className="w-3.5 h-3.5 text-purple-400 shrink-0" />
157
+ <span className="flex-1 truncate">{p.name}</span>
158
+ <CommandShortcut>plan</CommandShortcut>
159
+ </CommandItem>
160
+ ))}
161
+ </CommandGroup>
162
+ </>
163
+ )}
164
+
165
+ {memories.length > 0 && (
166
+ <>
167
+ <CommandSeparator />
168
+ <CommandGroup heading="Memory">
169
+ {memories.slice(0, 40).map((m, i) => (
170
+ <CommandItem
171
+ key={`mem-${i}`}
172
+ value={`${m.name ?? ''} ${m.description ?? ''} ${(m.body ?? '').slice(0, 100)}`}
173
+ onSelect={() => navigate('/memory')}
174
+ >
175
+ <Brain className="w-3.5 h-3.5 text-amber-400 shrink-0" />
176
+ <span className="flex-1 truncate">{m.name}</span>
177
+ <CommandShortcut>{m.type}</CommandShortcut>
178
+ </CommandItem>
179
+ ))}
180
+ </CommandGroup>
181
+ </>
182
+ )}
183
+ </CommandList>
184
+
185
+ <div className="border-t border-border px-3 py-2 flex items-center gap-4 text-[10px] text-muted-foreground/40 font-mono">
186
+ <span>↑↓ navigate</span>
187
+ <span>↵ open</span>
188
+ <span>esc close</span>
189
+ <span className="ml-auto">⌘K toggle</span>
190
+ </div>
191
+ </CommandDialog>
192
+ )
193
+ }
@@ -0,0 +1,23 @@
1
+ 'use client'
2
+
3
+ import { GlobalSearch } from './global-search'
4
+ import { useGlobalKeyboardNav } from './use-global-keyboard-nav'
5
+
6
+ function GModeIndicator() {
7
+ const gMode = useGlobalKeyboardNav()
8
+ if (!gMode) return null
9
+ return (
10
+ <div className="fixed bottom-20 right-4 md:bottom-4 z-50 px-3 py-1.5 bg-background border border-primary/60 rounded text-sm font-mono text-primary shadow-lg animate-in fade-in-0 duration-100 pointer-events-none">
11
+ g —
12
+ </div>
13
+ )
14
+ }
15
+
16
+ export function KeyboardNavProvider() {
17
+ return (
18
+ <>
19
+ <GlobalSearch />
20
+ <GModeIndicator />
21
+ </>
22
+ )
23
+ }
@@ -0,0 +1,52 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+ import { usePathname } from 'next/navigation'
5
+ import {
6
+ LayoutDashboard, MessageSquare, DollarSign,
7
+ FolderOpen, Activity, Moon, Sun,
8
+ } from 'lucide-react'
9
+ import { useTheme } from '@/components/theme-provider'
10
+ import { cn } from '@/lib/utils'
11
+
12
+ const NAV = [
13
+ { href: '/', label: 'Overview', icon: LayoutDashboard },
14
+ { href: '/sessions', label: 'Sessions', icon: MessageSquare },
15
+ { href: '/costs', label: 'Costs', icon: DollarSign },
16
+ { href: '/projects', label: 'Projects', icon: FolderOpen },
17
+ { href: '/activity', label: 'Activity', icon: Activity },
18
+ ]
19
+
20
+ export function BottomNav() {
21
+ const pathname = usePathname()
22
+ const { theme, toggle } = useTheme()
23
+
24
+ return (
25
+ <nav className="md:hidden fixed bottom-0 left-0 right-0 z-50 bg-sidebar border-t border-sidebar-border flex">
26
+ {NAV.map(({ href, label, icon: Icon }) => {
27
+ const active = pathname === href
28
+ return (
29
+ <Link
30
+ key={href}
31
+ href={href}
32
+ className={cn(
33
+ 'flex-1 flex flex-col items-center justify-center py-2.5 gap-1 transition-colors',
34
+ active ? 'text-sidebar-primary' : 'text-sidebar-foreground/40 hover:text-sidebar-foreground',
35
+ )}
36
+ >
37
+ <Icon className="w-4 h-4" />
38
+ <span className="text-[10px] font-medium leading-none">{label}</span>
39
+ </Link>
40
+ )
41
+ })}
42
+ <button
43
+ onClick={toggle}
44
+ aria-label="Toggle theme"
45
+ className="flex-1 flex flex-col items-center justify-center py-2.5 gap-1 transition-colors text-sidebar-foreground/40 hover:text-sidebar-foreground cursor-pointer"
46
+ >
47
+ {theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
48
+ <span className="text-[10px] font-medium leading-none">Theme</span>
49
+ </button>
50
+ </nav>
51
+ )
52
+ }
@@ -0,0 +1,31 @@
1
+ 'use client'
2
+
3
+ import { useSidebar } from '@/components/layout/sidebar-context'
4
+
5
+ export function ClientLayout({ children }: { children: React.ReactNode }) {
6
+ const { collapsed } = useSidebar()
7
+ return (
8
+ <main
9
+ className={[
10
+ 'flex-1 min-h-screen overflow-x-hidden bg-background pb-16 md:pb-0',
11
+ 'transition-[margin] duration-300',
12
+ collapsed ? 'md:ml-14' : 'md:ml-56',
13
+ ].join(' ')}
14
+ >
15
+ {children}
16
+ <footer className="border-t border-border/50 py-3 px-6 flex items-center justify-center mb-16 md:mb-0">
17
+ <p className="text-xs text-muted-foreground">
18
+ Made by{' '}
19
+ <a
20
+ href="https://github.com/Arindam200"
21
+ target="_blank"
22
+ rel="noopener noreferrer"
23
+ className="text-muted-foreground hover:text-foreground underline underline-offset-2 transition-colors"
24
+ >
25
+ Arindam
26
+ </a>
27
+ </p>
28
+ </footer>
29
+ </main>
30
+ )
31
+ }
@@ -0,0 +1,50 @@
1
+ 'use client'
2
+
3
+ import { createContext, useContext, useEffect, useState } from 'react'
4
+
5
+ interface SidebarContextType {
6
+ collapsed: boolean
7
+ toggle: () => void
8
+ mobileOpen: boolean
9
+ setMobileOpen: (v: boolean) => void
10
+ }
11
+
12
+ const SidebarContext = createContext<SidebarContextType>({
13
+ collapsed: false,
14
+ toggle: () => {},
15
+ mobileOpen: false,
16
+ setMobileOpen: () => {},
17
+ })
18
+
19
+ export function SidebarProvider({ children }: { children: React.ReactNode }) {
20
+ const [collapsed, setCollapsed] = useState(false)
21
+ const [mobileOpen, setMobileOpen] = useState(false)
22
+
23
+ useEffect(() => {
24
+ const id = window.setTimeout(() => {
25
+ try {
26
+ setCollapsed(localStorage.getItem('cc-sidebar-collapsed') === 'true')
27
+ } catch {}
28
+ }, 0)
29
+
30
+ return () => window.clearTimeout(id)
31
+ }, [])
32
+
33
+ function toggle() {
34
+ setCollapsed(prev => {
35
+ const next = !prev
36
+ try { localStorage.setItem('cc-sidebar-collapsed', String(next)) } catch {}
37
+ return next
38
+ })
39
+ }
40
+
41
+ return (
42
+ <SidebarContext.Provider value={{ collapsed, toggle, mobileOpen, setMobileOpen }}>
43
+ {children}
44
+ </SidebarContext.Provider>
45
+ )
46
+ }
47
+
48
+ export function useSidebar() {
49
+ return useContext(SidebarContext)
50
+ }