@axplusb/kepler 0.0.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +101 -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,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
|
+
}
|