@bahulam/code 2.6.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 (273) hide show
  1. package/README.md +80 -0
  2. package/package.json +49 -0
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/benchmark/route.ts +113 -0
  6. package/pulse/app/api/benchmarks/route.ts +195 -0
  7. package/pulse/app/api/costs/route.ts +88 -0
  8. package/pulse/app/api/export/route.ts +77 -0
  9. package/pulse/app/api/history/route.ts +11 -0
  10. package/pulse/app/api/import/route.ts +31 -0
  11. package/pulse/app/api/memory/route.ts +50 -0
  12. package/pulse/app/api/plans/route.ts +9 -0
  13. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  14. package/pulse/app/api/projects/route.ts +121 -0
  15. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  16. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  17. package/pulse/app/api/sessions/route.ts +112 -0
  18. package/pulse/app/api/settings/route.ts +14 -0
  19. package/pulse/app/api/stats/route.ts +143 -0
  20. package/pulse/app/api/todos/route.ts +9 -0
  21. package/pulse/app/api/tools/route.ts +160 -0
  22. package/pulse/app/benchmarks/page.tsx +224 -0
  23. package/pulse/app/costs/page.tsx +179 -0
  24. package/pulse/app/export/page.tsx +465 -0
  25. package/pulse/app/favicon.ico +0 -0
  26. package/pulse/app/globals.css +263 -0
  27. package/pulse/app/help/page.tsx +143 -0
  28. package/pulse/app/history/page.tsx +157 -0
  29. package/pulse/app/layout.tsx +46 -0
  30. package/pulse/app/memory/page.tsx +365 -0
  31. package/pulse/app/overview-client.tsx +393 -0
  32. package/pulse/app/page.tsx +14 -0
  33. package/pulse/app/plans/page.tsx +308 -0
  34. package/pulse/app/projects/[slug]/page.tsx +390 -0
  35. package/pulse/app/projects/page.tsx +110 -0
  36. package/pulse/app/sessions/[id]/page.tsx +243 -0
  37. package/pulse/app/sessions/page.tsx +39 -0
  38. package/pulse/app/settings/page.tsx +188 -0
  39. package/pulse/app/todos/page.tsx +211 -0
  40. package/pulse/app/tools/page.tsx +249 -0
  41. package/pulse/cli.js +164 -0
  42. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  43. package/pulse/components/activity/streak-card.tsx +36 -0
  44. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  45. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  46. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  47. package/pulse/components/costs/model-token-table.tsx +60 -0
  48. package/pulse/components/global-search.tsx +193 -0
  49. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  50. package/pulse/components/layout/bottom-nav.tsx +53 -0
  51. package/pulse/components/layout/client-layout.tsx +31 -0
  52. package/pulse/components/layout/sidebar-context.tsx +50 -0
  53. package/pulse/components/layout/sidebar.tsx +183 -0
  54. package/pulse/components/layout/top-bar.tsx +121 -0
  55. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  56. package/pulse/components/overview/conversation-table.tsx +148 -0
  57. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  58. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  59. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  60. package/pulse/components/overview/stat-card.tsx +102 -0
  61. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  62. package/pulse/components/projects/project-card.tsx +175 -0
  63. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  64. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  65. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  66. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  67. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  68. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  69. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  70. package/pulse/components/sessions/session-badges.tsx +49 -0
  71. package/pulse/components/sessions/session-table.tsx +299 -0
  72. package/pulse/components/theme-provider.tsx +44 -0
  73. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  74. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  75. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  76. package/pulse/components/tools/version-history-table.tsx +32 -0
  77. package/pulse/components/ui/alert.tsx +66 -0
  78. package/pulse/components/ui/badge.tsx +48 -0
  79. package/pulse/components/ui/breadcrumb.tsx +109 -0
  80. package/pulse/components/ui/button.tsx +64 -0
  81. package/pulse/components/ui/calendar.tsx +220 -0
  82. package/pulse/components/ui/card.tsx +92 -0
  83. package/pulse/components/ui/command.tsx +158 -0
  84. package/pulse/components/ui/dialog.tsx +158 -0
  85. package/pulse/components/ui/input.tsx +21 -0
  86. package/pulse/components/ui/popover.tsx +89 -0
  87. package/pulse/components/ui/progress.tsx +31 -0
  88. package/pulse/components/ui/select.tsx +190 -0
  89. package/pulse/components/ui/separator.tsx +28 -0
  90. package/pulse/components/ui/sheet.tsx +143 -0
  91. package/pulse/components/ui/skeleton.tsx +13 -0
  92. package/pulse/components/ui/table.tsx +116 -0
  93. package/pulse/components/ui/tabs.tsx +91 -0
  94. package/pulse/components/ui/tooltip.tsx +57 -0
  95. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  96. package/pulse/components.json +23 -0
  97. package/pulse/eslint.config.mjs +18 -0
  98. package/pulse/lib/bahulam-paths.ts +23 -0
  99. package/pulse/lib/claude-reader.ts +592 -0
  100. package/pulse/lib/decode.ts +129 -0
  101. package/pulse/lib/pricing.ts +102 -0
  102. package/pulse/lib/replay-parser.ts +165 -0
  103. package/pulse/lib/tool-categories.ts +127 -0
  104. package/pulse/lib/utils.ts +6 -0
  105. package/pulse/next-env.d.ts +6 -0
  106. package/pulse/next.config.ts +16 -0
  107. package/pulse/package.json +45 -0
  108. package/pulse/postcss.config.mjs +7 -0
  109. package/pulse/public/activity.png +0 -0
  110. package/pulse/public/cc-lens.png +0 -0
  111. package/pulse/public/command-k.png +0 -0
  112. package/pulse/public/costs.png +0 -0
  113. package/pulse/public/dashboard-dark.png +0 -0
  114. package/pulse/public/dashboard-white.png +0 -0
  115. package/pulse/public/export.png +0 -0
  116. package/pulse/public/file.svg +1 -0
  117. package/pulse/public/globe.svg +1 -0
  118. package/pulse/public/next.svg +1 -0
  119. package/pulse/public/projects.png +0 -0
  120. package/pulse/public/session-chat.png +0 -0
  121. package/pulse/public/todos.png +0 -0
  122. package/pulse/public/tools.png +0 -0
  123. package/pulse/public/vercel.svg +1 -0
  124. package/pulse/public/window.svg +1 -0
  125. package/pulse/tsconfig.json +34 -0
  126. package/pulse/types/claude.ts +294 -0
  127. package/src/agents/loader.mjs +94 -0
  128. package/src/agents/multi_workflow_loader.mjs +330 -0
  129. package/src/agents/parser.mjs +205 -0
  130. package/src/agents/scaffold.mjs +222 -0
  131. package/src/agents/teams.mjs +123 -0
  132. package/src/agents/workflow_loader.mjs +122 -0
  133. package/src/agents/workflow_scaffold.mjs +249 -0
  134. package/src/auth/oauth.mjs +220 -0
  135. package/src/auth/tarang-auth.mjs +306 -0
  136. package/src/commands/agent.mjs +220 -0
  137. package/src/commands/workflow.mjs +581 -0
  138. package/src/config/cli-args.mjs +200 -0
  139. package/src/config/env.mjs +263 -0
  140. package/src/config/hook-runner.mjs +100 -0
  141. package/src/config/memory-loader.mjs +32 -0
  142. package/src/config/settings-loader.mjs +45 -0
  143. package/src/config/settings.mjs +132 -0
  144. package/src/context/ast-parser.mjs +298 -0
  145. package/src/context/bm25.mjs +85 -0
  146. package/src/context/retriever.mjs +308 -0
  147. package/src/context/skeleton.mjs +134 -0
  148. package/src/context/symbol-indexer.mjs +375 -0
  149. package/src/core/agent-history.mjs +111 -0
  150. package/src/core/agent-loop.mjs +486 -0
  151. package/src/core/approval-log.mjs +104 -0
  152. package/src/core/approval.mjs +476 -0
  153. package/src/core/attachments.mjs +380 -0
  154. package/src/core/backend-url.mjs +55 -0
  155. package/src/core/cache-control.mjs +92 -0
  156. package/src/core/cache.mjs +105 -0
  157. package/src/core/callback-client.mjs +180 -0
  158. package/src/core/checkpoints.mjs +142 -0
  159. package/src/core/compact-history.mjs +127 -0
  160. package/src/core/context-envelope.mjs +54 -0
  161. package/src/core/context-manager.mjs +198 -0
  162. package/src/core/error-guidance.mjs +311 -0
  163. package/src/core/file-diff.mjs +217 -0
  164. package/src/core/headless.mjs +448 -0
  165. package/src/core/hooks-manager.mjs +87 -0
  166. package/src/core/jsonl-writer.mjs +449 -0
  167. package/src/core/local-agent.mjs +537 -0
  168. package/src/core/local-store.mjs +836 -0
  169. package/src/core/mode-selector.mjs +51 -0
  170. package/src/core/output-filter.mjs +177 -0
  171. package/src/core/paths.mjs +190 -0
  172. package/src/core/policy-resolver.mjs +156 -0
  173. package/src/core/pricing.mjs +336 -0
  174. package/src/core/project-artifacts.mjs +39 -0
  175. package/src/core/project-context-loader.mjs +139 -0
  176. package/src/core/providers.mjs +219 -0
  177. package/src/core/rate-limit-display.mjs +121 -0
  178. package/src/core/rate-limiter.mjs +119 -0
  179. package/src/core/resume-mode.mjs +192 -0
  180. package/src/core/risk-tier.mjs +337 -0
  181. package/src/core/safety.mjs +203 -0
  182. package/src/core/scheduler.mjs +173 -0
  183. package/src/core/session-manager.mjs +360 -0
  184. package/src/core/session.mjs +143 -0
  185. package/src/core/settings-sync.mjs +85 -0
  186. package/src/core/stagnation.mjs +57 -0
  187. package/src/core/stream-client.mjs +829 -0
  188. package/src/core/streaming.mjs +182 -0
  189. package/src/core/system-prompt.mjs +140 -0
  190. package/src/core/tasks.mjs +196 -0
  191. package/src/core/tool-executor.mjs +1950 -0
  192. package/src/core/trust.mjs +158 -0
  193. package/src/core/work-scope.mjs +248 -0
  194. package/src/hooks/engine.mjs +162 -0
  195. package/src/index.mjs +426 -0
  196. package/src/mcp/client.mjs +253 -0
  197. package/src/mcp/transport-shttp.mjs +130 -0
  198. package/src/mcp/transport-sse.mjs +131 -0
  199. package/src/mcp/transport-ws.mjs +134 -0
  200. package/src/onboarding/preflight.mjs +360 -0
  201. package/src/permissions/checker.mjs +57 -0
  202. package/src/permissions/command-classifier.mjs +652 -0
  203. package/src/permissions/injection-check.mjs +60 -0
  204. package/src/permissions/path-check.mjs +102 -0
  205. package/src/permissions/prompt.mjs +73 -0
  206. package/src/permissions/sandbox.mjs +112 -0
  207. package/src/plugins/loader.mjs +138 -0
  208. package/src/skills/installer.mjs +188 -0
  209. package/src/skills/loader.mjs +252 -0
  210. package/src/skills/runner.mjs +55 -0
  211. package/src/state/orbit.mjs +263 -0
  212. package/src/state/verbosity.mjs +99 -0
  213. package/src/telemetry/index.mjs +96 -0
  214. package/src/terminal/agents.mjs +177 -0
  215. package/src/terminal/analytics.mjs +292 -0
  216. package/src/terminal/ansi.mjs +695 -0
  217. package/src/terminal/init.mjs +145 -0
  218. package/src/terminal/main.mjs +269 -0
  219. package/src/terminal/repl-explore.mjs +35 -0
  220. package/src/terminal/repl-format.mjs +257 -0
  221. package/src/terminal/repl-render.mjs +561 -0
  222. package/src/terminal/repl-resume.mjs +625 -0
  223. package/src/terminal/repl-state.mjs +103 -0
  224. package/src/terminal/repl-utils.mjs +34 -0
  225. package/src/terminal/repl.mjs +3832 -0
  226. package/src/terminal/skills.mjs +54 -0
  227. package/src/terminal/tool-display.mjs +240 -0
  228. package/src/tools/agent.mjs +137 -0
  229. package/src/tools/ask-user.mjs +61 -0
  230. package/src/tools/bash.mjs +231 -0
  231. package/src/tools/cron-create.mjs +120 -0
  232. package/src/tools/cron-delete.mjs +49 -0
  233. package/src/tools/cron-list.mjs +37 -0
  234. package/src/tools/edit.mjs +82 -0
  235. package/src/tools/enter-worktree.mjs +69 -0
  236. package/src/tools/exit-worktree.mjs +57 -0
  237. package/src/tools/glob.mjs +117 -0
  238. package/src/tools/grep.mjs +129 -0
  239. package/src/tools/lint.mjs +71 -0
  240. package/src/tools/ls.mjs +58 -0
  241. package/src/tools/lsp.mjs +115 -0
  242. package/src/tools/multi-edit.mjs +94 -0
  243. package/src/tools/notebook-edit.mjs +96 -0
  244. package/src/tools/project-overview.mjs +641 -0
  245. package/src/tools/read-mcp-resource.mjs +57 -0
  246. package/src/tools/read.mjs +138 -0
  247. package/src/tools/registry.mjs +116 -0
  248. package/src/tools/remote-trigger.mjs +84 -0
  249. package/src/tools/send-message.mjs +64 -0
  250. package/src/tools/skill.mjs +52 -0
  251. package/src/tools/test-runner.mjs +49 -0
  252. package/src/tools/todo-write.mjs +68 -0
  253. package/src/tools/tool-search.mjs +77 -0
  254. package/src/tools/web-fetch.mjs +65 -0
  255. package/src/tools/web-search.mjs +89 -0
  256. package/src/tools/write.mjs +55 -0
  257. package/src/ui/approval.mjs +263 -0
  258. package/src/ui/banner.mjs +235 -0
  259. package/src/ui/commands.mjs +537 -0
  260. package/src/ui/formatter.mjs +409 -0
  261. package/src/ui/icons.mjs +164 -0
  262. package/src/ui/input-dock.mjs +444 -0
  263. package/src/ui/markdown.mjs +278 -0
  264. package/src/ui/mission-report.mjs +296 -0
  265. package/src/ui/palette.mjs +189 -0
  266. package/src/ui/slash-commands.mjs +245 -0
  267. package/src/ui/spinner.mjs +116 -0
  268. package/src/ui/sub-agent.mjs +152 -0
  269. package/src/ui/term.mjs +159 -0
  270. package/src/ui/text-layout.mjs +127 -0
  271. package/src/ui/tool-card.mjs +463 -0
  272. package/src/ui/tool-details.mjs +312 -0
  273. package/src/ui/transcript-block.mjs +21 -0
@@ -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,53 @@
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, Zap,
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
+ { href: '/benchmarks', label: 'Benchmarks', icon: Zap },
19
+ ]
20
+
21
+ export function BottomNav() {
22
+ const pathname = usePathname()
23
+ const { theme, toggle } = useTheme()
24
+
25
+ return (
26
+ <nav className="md:hidden fixed bottom-0 left-0 right-0 z-50 bg-sidebar border-t border-sidebar-border flex">
27
+ {NAV.map(({ href, label, icon: Icon }) => {
28
+ const active = pathname === href
29
+ return (
30
+ <Link
31
+ key={href}
32
+ href={href}
33
+ className={cn(
34
+ 'flex-1 flex flex-col items-center justify-center py-2.5 gap-1 transition-colors',
35
+ active ? 'text-sidebar-primary' : 'text-sidebar-foreground/40 hover:text-sidebar-foreground',
36
+ )}
37
+ >
38
+ <Icon className="w-4 h-4" />
39
+ <span className="text-[10px] font-medium leading-none">{label}</span>
40
+ </Link>
41
+ )
42
+ })}
43
+ <button
44
+ onClick={toggle}
45
+ aria-label="Toggle theme"
46
+ 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"
47
+ >
48
+ {theme === 'dark' ? <Sun className="w-4 h-4" /> : <Moon className="w-4 h-4" />}
49
+ <span className="text-[10px] font-medium leading-none">Theme</span>
50
+ </button>
51
+ </nav>
52
+ )
53
+ }
@@ -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
+ }