@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,390 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useParams } from 'next/navigation'
|
|
4
|
+
import useSWR from 'swr'
|
|
5
|
+
import Link from 'next/link'
|
|
6
|
+
import { TopBar } from '@/components/layout/top-bar'
|
|
7
|
+
import { formatCost, formatDuration, formatDate, formatTokens } from '@/lib/decode'
|
|
8
|
+
import { categoryColorMix, toolBarColor } from '@/lib/tool-categories'
|
|
9
|
+
import { LineChart, Line, XAxis, YAxis, Tooltip, CartesianGrid, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts'
|
|
10
|
+
import type { SessionWithFacet } from '@/types/claude'
|
|
11
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
12
|
+
import { Badge } from '@/components/ui/badge'
|
|
13
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
14
|
+
import { Alert, AlertDescription } from '@/components/ui/alert'
|
|
15
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
|
16
|
+
import {
|
|
17
|
+
Breadcrumb, BreadcrumbItem, BreadcrumbLink,
|
|
18
|
+
BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator,
|
|
19
|
+
} from '@/components/ui/breadcrumb'
|
|
20
|
+
import {
|
|
21
|
+
MessageSquare, Clock, DollarSign, GitBranch,
|
|
22
|
+
Wrench, TrendingUp, AlertTriangle, Code2,
|
|
23
|
+
} from 'lucide-react'
|
|
24
|
+
|
|
25
|
+
const fetcher = (url: string) =>
|
|
26
|
+
fetch(url).then(r => { if (!r.ok) throw new Error(`API error ${r.status}`); return r.json() })
|
|
27
|
+
|
|
28
|
+
interface ProjectDetail {
|
|
29
|
+
project_path: string
|
|
30
|
+
display_name: string
|
|
31
|
+
sessions: SessionWithFacet[]
|
|
32
|
+
tool_counts: Record<string, number>
|
|
33
|
+
cost_by_session: Array<{ session_id: string; start_time: string; cost: number; messages: number }>
|
|
34
|
+
branches: Array<{ branch: string; turns: number }>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const LANG_CHART_COLORS = ['#d97706', 'var(--viz-sky)', '#34d399', '#a78bfa', '#fbbf24', '#f87171']
|
|
38
|
+
|
|
39
|
+
export default function ProjectDetailPage() {
|
|
40
|
+
const params = useParams()
|
|
41
|
+
const slug = params?.slug as string
|
|
42
|
+
|
|
43
|
+
const { data, error, isLoading } = useSWR<ProjectDetail>(
|
|
44
|
+
slug ? `/api/projects/${slug}` : null, fetcher
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if (error) {
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex flex-col min-h-screen">
|
|
50
|
+
<TopBar title="Project" />
|
|
51
|
+
<div className="p-6">
|
|
52
|
+
<Alert variant="destructive">
|
|
53
|
+
<AlertTriangle className="h-4 w-4" />
|
|
54
|
+
<AlertDescription>Error loading project: {String(error)}</AlertDescription>
|
|
55
|
+
</Alert>
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (isLoading || !data) {
|
|
62
|
+
return (
|
|
63
|
+
<div className="flex flex-col min-h-screen">
|
|
64
|
+
<TopBar title="Project" subtitle="Loading…" />
|
|
65
|
+
<div className="p-6 space-y-4">
|
|
66
|
+
<Skeleton className="h-5 w-48" />
|
|
67
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
68
|
+
{Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-28 rounded-xl" />)}
|
|
69
|
+
</div>
|
|
70
|
+
<Skeleton className="h-64 rounded-xl" />
|
|
71
|
+
<div className="grid grid-cols-2 gap-4">
|
|
72
|
+
<Skeleton className="h-48 rounded-xl" />
|
|
73
|
+
<Skeleton className="h-48 rounded-xl" />
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ── Derived data ────────────────────────────────────────────────────────────
|
|
81
|
+
const sessions = data.sessions ?? []
|
|
82
|
+
const costBySessions = data.cost_by_session ?? []
|
|
83
|
+
const branches = data.branches ?? []
|
|
84
|
+
|
|
85
|
+
const totalCost = sessions.reduce((s, x) => s + (x.estimated_cost ?? 0), 0)
|
|
86
|
+
const totalMsgs = sessions.reduce((s, x) => s + (x.user_message_count ?? 0) + (x.assistant_message_count ?? 0), 0)
|
|
87
|
+
const totalDuration = sessions.reduce((s, x) => s + (x.duration_minutes ?? 0), 0)
|
|
88
|
+
const totalTokens = sessions.reduce((s, x) => s + (x.input_tokens ?? 0) + (x.output_tokens ?? 0), 0)
|
|
89
|
+
|
|
90
|
+
const topTools = Object.entries(data.tool_counts ?? {})
|
|
91
|
+
.sort(([, a], [, b]) => b - a).slice(0, 12)
|
|
92
|
+
const maxToolCount = topTools[0]?.[1] ?? 1
|
|
93
|
+
|
|
94
|
+
const langMap: Record<string, number> = {}
|
|
95
|
+
for (const s of sessions) {
|
|
96
|
+
for (const [lang, count] of Object.entries(s.languages ?? {})) {
|
|
97
|
+
langMap[lang] = (langMap[lang] ?? 0) + count
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const topLangs = Object.entries(langMap).sort(([, a], [, b]) => b - a).slice(0, 6)
|
|
101
|
+
const maxBranchTurns = branches[0]?.turns ?? 1
|
|
102
|
+
|
|
103
|
+
return (
|
|
104
|
+
<div className="flex flex-col min-h-screen">
|
|
105
|
+
<TopBar title={data.display_name} subtitle={data.project_path} />
|
|
106
|
+
|
|
107
|
+
<div className="p-6 space-y-6">
|
|
108
|
+
{/* Breadcrumb */}
|
|
109
|
+
<Breadcrumb>
|
|
110
|
+
<BreadcrumbList>
|
|
111
|
+
<BreadcrumbItem>
|
|
112
|
+
<BreadcrumbLink asChild>
|
|
113
|
+
<Link href="/projects">Projects</Link>
|
|
114
|
+
</BreadcrumbLink>
|
|
115
|
+
</BreadcrumbItem>
|
|
116
|
+
<BreadcrumbSeparator />
|
|
117
|
+
<BreadcrumbItem>
|
|
118
|
+
<BreadcrumbPage>{data.display_name}</BreadcrumbPage>
|
|
119
|
+
</BreadcrumbItem>
|
|
120
|
+
</BreadcrumbList>
|
|
121
|
+
</Breadcrumb>
|
|
122
|
+
|
|
123
|
+
{/* Stat cards */}
|
|
124
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
|
125
|
+
<Card>
|
|
126
|
+
<CardHeader className="pb-2">
|
|
127
|
+
<CardDescription className="flex items-center gap-2">
|
|
128
|
+
<MessageSquare className="w-4 h-4" /> Sessions
|
|
129
|
+
</CardDescription>
|
|
130
|
+
<CardTitle className="text-3xl font-bold tabular-nums">{sessions.length}</CardTitle>
|
|
131
|
+
</CardHeader>
|
|
132
|
+
<CardContent>
|
|
133
|
+
<p className="text-xs text-muted-foreground">{totalMsgs.toLocaleString()} messages</p>
|
|
134
|
+
</CardContent>
|
|
135
|
+
</Card>
|
|
136
|
+
|
|
137
|
+
<Card>
|
|
138
|
+
<CardHeader className="pb-2">
|
|
139
|
+
<CardDescription className="flex items-center gap-2">
|
|
140
|
+
<Clock className="w-4 h-4" /> Duration
|
|
141
|
+
</CardDescription>
|
|
142
|
+
<CardTitle className="text-3xl font-bold tabular-nums">{formatDuration(totalDuration)}</CardTitle>
|
|
143
|
+
</CardHeader>
|
|
144
|
+
<CardContent>
|
|
145
|
+
<p className="text-xs text-muted-foreground">Total time</p>
|
|
146
|
+
</CardContent>
|
|
147
|
+
</Card>
|
|
148
|
+
|
|
149
|
+
<Card>
|
|
150
|
+
<CardHeader className="pb-2">
|
|
151
|
+
<CardDescription className="flex items-center gap-2">
|
|
152
|
+
<TrendingUp className="w-4 h-4" /> Tokens
|
|
153
|
+
</CardDescription>
|
|
154
|
+
<CardTitle className="text-3xl font-bold tabular-nums text-blue-700 dark:text-[#60a5fa]">
|
|
155
|
+
{formatTokens(totalTokens)}
|
|
156
|
+
</CardTitle>
|
|
157
|
+
</CardHeader>
|
|
158
|
+
<CardContent>
|
|
159
|
+
<p className="text-xs text-muted-foreground">Input + output</p>
|
|
160
|
+
</CardContent>
|
|
161
|
+
</Card>
|
|
162
|
+
|
|
163
|
+
<Card>
|
|
164
|
+
<CardHeader className="pb-2">
|
|
165
|
+
<CardDescription className="flex items-center gap-2">
|
|
166
|
+
<DollarSign className="w-4 h-4" /> Est. Cost
|
|
167
|
+
</CardDescription>
|
|
168
|
+
<CardTitle className="text-3xl font-bold tabular-nums text-[#d97706]">
|
|
169
|
+
{formatCost(totalCost)}
|
|
170
|
+
</CardTitle>
|
|
171
|
+
</CardHeader>
|
|
172
|
+
<CardContent>
|
|
173
|
+
<p className="text-xs text-muted-foreground">All sessions</p>
|
|
174
|
+
</CardContent>
|
|
175
|
+
</Card>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{/* Sessions table + Tool sidebar */}
|
|
179
|
+
<div className="grid grid-cols-1 lg:grid-cols-[1fr_280px] gap-6">
|
|
180
|
+
<Card className="gap-0 py-8">
|
|
181
|
+
<CardHeader className="space-y-1.5 px-8 pb-5 pt-2">
|
|
182
|
+
<CardTitle>Sessions</CardTitle>
|
|
183
|
+
<CardDescription>{sessions.length} conversations in this project</CardDescription>
|
|
184
|
+
</CardHeader>
|
|
185
|
+
<CardContent className="px-8 pb-2 pt-0">
|
|
186
|
+
<Table>
|
|
187
|
+
<TableHeader>
|
|
188
|
+
<TableRow>
|
|
189
|
+
<TableHead>Date</TableHead>
|
|
190
|
+
<TableHead>Slug</TableHead>
|
|
191
|
+
<TableHead className="text-right">Msgs</TableHead>
|
|
192
|
+
<TableHead className="text-right">Cost</TableHead>
|
|
193
|
+
</TableRow>
|
|
194
|
+
</TableHeader>
|
|
195
|
+
<TableBody>
|
|
196
|
+
{sessions.map(s => {
|
|
197
|
+
const msgs = (s.user_message_count ?? 0) + (s.assistant_message_count ?? 0)
|
|
198
|
+
return (
|
|
199
|
+
<TableRow key={s.session_id}>
|
|
200
|
+
<TableCell className="text-muted-foreground whitespace-nowrap text-sm">
|
|
201
|
+
{formatDate(s.start_time)}
|
|
202
|
+
</TableCell>
|
|
203
|
+
<TableCell>
|
|
204
|
+
<Link
|
|
205
|
+
href={`/sessions/${s.session_id}`}
|
|
206
|
+
className="text-foreground hover:text-primary transition-colors font-medium text-sm"
|
|
207
|
+
>
|
|
208
|
+
{s.slug ?? s.session_id.slice(0, 8) + '…'}
|
|
209
|
+
</Link>
|
|
210
|
+
</TableCell>
|
|
211
|
+
<TableCell className="text-right tabular-nums text-muted-foreground">{msgs}</TableCell>
|
|
212
|
+
<TableCell className="text-right tabular-nums text-[#d97706] font-mono font-medium">
|
|
213
|
+
{formatCost(s.estimated_cost)}
|
|
214
|
+
</TableCell>
|
|
215
|
+
</TableRow>
|
|
216
|
+
)
|
|
217
|
+
})}
|
|
218
|
+
{sessions.length === 0 && (
|
|
219
|
+
<TableRow>
|
|
220
|
+
<TableCell colSpan={4} className="text-center text-muted-foreground py-8">No sessions yet</TableCell>
|
|
221
|
+
</TableRow>
|
|
222
|
+
)}
|
|
223
|
+
</TableBody>
|
|
224
|
+
</Table>
|
|
225
|
+
</CardContent>
|
|
226
|
+
</Card>
|
|
227
|
+
|
|
228
|
+
<Card>
|
|
229
|
+
<CardHeader>
|
|
230
|
+
<div className="flex items-start justify-between">
|
|
231
|
+
<div>
|
|
232
|
+
<CardTitle>Most-Used Tools</CardTitle>
|
|
233
|
+
<CardDescription>Top {topTools.length} tools</CardDescription>
|
|
234
|
+
</div>
|
|
235
|
+
<Wrench className="w-4 h-4 text-muted-foreground mt-0.5" />
|
|
236
|
+
</div>
|
|
237
|
+
</CardHeader>
|
|
238
|
+
<CardContent>
|
|
239
|
+
<div className="space-y-2">
|
|
240
|
+
{topTools.map(([tool, count]) => {
|
|
241
|
+
const color = toolBarColor(tool)
|
|
242
|
+
const width = Math.max(4, Math.round((count / maxToolCount) * 100))
|
|
243
|
+
return (
|
|
244
|
+
<div key={tool} className="flex items-center gap-2">
|
|
245
|
+
<span className="text-xs text-muted-foreground w-20 truncate">{tool}</span>
|
|
246
|
+
<div className="flex-1 h-1.5 bg-muted rounded-full overflow-hidden">
|
|
247
|
+
<div
|
|
248
|
+
className="h-full rounded-full"
|
|
249
|
+
style={{ width: `${width}%`, backgroundColor: categoryColorMix(color, 58) }}
|
|
250
|
+
/>
|
|
251
|
+
</div>
|
|
252
|
+
<span className="text-xs text-muted-foreground/60 w-7 text-right tabular-nums">{count}</span>
|
|
253
|
+
</div>
|
|
254
|
+
)
|
|
255
|
+
})}
|
|
256
|
+
{topTools.length === 0 && (
|
|
257
|
+
<p className="text-sm text-muted-foreground">No tool data</p>
|
|
258
|
+
)}
|
|
259
|
+
</div>
|
|
260
|
+
</CardContent>
|
|
261
|
+
</Card>
|
|
262
|
+
</div>
|
|
263
|
+
|
|
264
|
+
{/* Cost per session chart */}
|
|
265
|
+
{costBySessions.length > 1 && (
|
|
266
|
+
<Card>
|
|
267
|
+
<CardHeader>
|
|
268
|
+
<div className="flex items-start justify-between">
|
|
269
|
+
<div>
|
|
270
|
+
<CardTitle>Cost Per Session</CardTitle>
|
|
271
|
+
<CardDescription>Estimated spend over time</CardDescription>
|
|
272
|
+
</div>
|
|
273
|
+
<DollarSign className="w-4 h-4 text-muted-foreground mt-0.5" />
|
|
274
|
+
</div>
|
|
275
|
+
</CardHeader>
|
|
276
|
+
<CardContent>
|
|
277
|
+
<ResponsiveContainer width="100%" height={220}>
|
|
278
|
+
<LineChart
|
|
279
|
+
data={costBySessions.map(s => ({ date: s.start_time.slice(0, 10), cost: s.cost }))}
|
|
280
|
+
margin={{ top: 8, right: 16, bottom: 24, left: 8 }}
|
|
281
|
+
>
|
|
282
|
+
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" vertical={false} />
|
|
283
|
+
<XAxis
|
|
284
|
+
dataKey="date"
|
|
285
|
+
tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }}
|
|
286
|
+
tickLine={false}
|
|
287
|
+
axisLine={false}
|
|
288
|
+
interval="preserveStartEnd"
|
|
289
|
+
height={36}
|
|
290
|
+
/>
|
|
291
|
+
<YAxis
|
|
292
|
+
tick={{ fontSize: 11, fill: 'var(--muted-foreground)' }}
|
|
293
|
+
tickLine={false}
|
|
294
|
+
axisLine={false}
|
|
295
|
+
tickFormatter={v => `$${v.toFixed(2)}`}
|
|
296
|
+
width={52}
|
|
297
|
+
/>
|
|
298
|
+
<Tooltip
|
|
299
|
+
contentStyle={{ background: 'var(--card)', border: '1px solid var(--border)', borderRadius: 8, fontSize: 12 }}
|
|
300
|
+
formatter={(v: unknown) => [formatCost(v as number), 'Cost']}
|
|
301
|
+
/>
|
|
302
|
+
<Line type="monotone" dataKey="cost" stroke="#d97706" strokeWidth={2} dot={{ r: 3, fill: '#d97706' }} activeDot={{ r: 5 }} />
|
|
303
|
+
</LineChart>
|
|
304
|
+
</ResponsiveContainer>
|
|
305
|
+
</CardContent>
|
|
306
|
+
</Card>
|
|
307
|
+
)}
|
|
308
|
+
|
|
309
|
+
{/* Languages + Branches — two columns only when both exist; otherwise full width so one card isn’t stranded half-row */}
|
|
310
|
+
<div
|
|
311
|
+
className={
|
|
312
|
+
topLangs.length > 0 && branches.length > 0
|
|
313
|
+
? 'grid grid-cols-1 gap-6 md:grid-cols-2'
|
|
314
|
+
: 'grid grid-cols-1 gap-6'
|
|
315
|
+
}
|
|
316
|
+
>
|
|
317
|
+
{topLangs.length > 0 && (
|
|
318
|
+
<Card>
|
|
319
|
+
<CardHeader>
|
|
320
|
+
<div className="flex items-start justify-between">
|
|
321
|
+
<div>
|
|
322
|
+
<CardTitle>Languages</CardTitle>
|
|
323
|
+
<CardDescription>Files touched across sessions</CardDescription>
|
|
324
|
+
</div>
|
|
325
|
+
<Code2 className="w-4 h-4 text-muted-foreground mt-0.5" />
|
|
326
|
+
</div>
|
|
327
|
+
</CardHeader>
|
|
328
|
+
<CardContent>
|
|
329
|
+
<div className="flex items-center gap-6">
|
|
330
|
+
<ResponsiveContainer width={100} height={100}>
|
|
331
|
+
<PieChart>
|
|
332
|
+
<Pie
|
|
333
|
+
data={topLangs.map(([name, value]) => ({ name, value }))}
|
|
334
|
+
cx="50%" cy="50%"
|
|
335
|
+
innerRadius={28} outerRadius={46}
|
|
336
|
+
dataKey="value" strokeWidth={0}
|
|
337
|
+
>
|
|
338
|
+
{topLangs.map((_, i) => (
|
|
339
|
+
<Cell key={i} fill={LANG_CHART_COLORS[i % LANG_CHART_COLORS.length]} />
|
|
340
|
+
))}
|
|
341
|
+
</Pie>
|
|
342
|
+
</PieChart>
|
|
343
|
+
</ResponsiveContainer>
|
|
344
|
+
<div className="space-y-1.5">
|
|
345
|
+
{topLangs.map(([lang], i) => (
|
|
346
|
+
<div key={lang} className="flex items-center gap-2">
|
|
347
|
+
<span className="w-2 h-2 rounded-full shrink-0" style={{ backgroundColor: LANG_CHART_COLORS[i % LANG_CHART_COLORS.length] }} />
|
|
348
|
+
<Badge variant="outline" className="text-xs py-0">{lang}</Badge>
|
|
349
|
+
</div>
|
|
350
|
+
))}
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
</CardContent>
|
|
354
|
+
</Card>
|
|
355
|
+
)}
|
|
356
|
+
|
|
357
|
+
{branches.length > 0 && (
|
|
358
|
+
<Card>
|
|
359
|
+
<CardHeader>
|
|
360
|
+
<div className="flex items-start justify-between">
|
|
361
|
+
<div>
|
|
362
|
+
<CardTitle>Git Branches</CardTitle>
|
|
363
|
+
<CardDescription>Activity by branch</CardDescription>
|
|
364
|
+
</div>
|
|
365
|
+
<GitBranch className="w-4 h-4 text-muted-foreground mt-0.5" />
|
|
366
|
+
</div>
|
|
367
|
+
</CardHeader>
|
|
368
|
+
<CardContent>
|
|
369
|
+
<div className="space-y-2.5">
|
|
370
|
+
{branches.map(({ branch, turns }) => {
|
|
371
|
+
const width = Math.max(4, Math.round((turns / maxBranchTurns) * 100))
|
|
372
|
+
return (
|
|
373
|
+
<div key={branch} className="flex items-center gap-2">
|
|
374
|
+
<span className="text-xs text-muted-foreground/70 w-24 truncate font-mono">{branch}</span>
|
|
375
|
+
<div className="flex-1 h-1.5 bg-muted rounded-full overflow-hidden">
|
|
376
|
+
<div className="h-full rounded-full bg-emerald-500/60" style={{ width: `${width}%` }} />
|
|
377
|
+
</div>
|
|
378
|
+
<span className="text-xs text-muted-foreground/50 w-16 text-right tabular-nums">{turns} turns</span>
|
|
379
|
+
</div>
|
|
380
|
+
)
|
|
381
|
+
})}
|
|
382
|
+
</div>
|
|
383
|
+
</CardContent>
|
|
384
|
+
</Card>
|
|
385
|
+
)}
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
)
|
|
390
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from 'react'
|
|
4
|
+
import useSWR from 'swr'
|
|
5
|
+
import { TopBar } from '@/components/layout/top-bar'
|
|
6
|
+
import { ProjectCard } from '@/components/projects/project-card'
|
|
7
|
+
import type { ProjectSummary } from '@/types/claude'
|
|
8
|
+
import { Input } from '@/components/ui/input'
|
|
9
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
10
|
+
import { Alert, AlertDescription } from '@/components/ui/alert'
|
|
11
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
|
12
|
+
import { Search, AlertTriangle, ArrowUpDown } from 'lucide-react'
|
|
13
|
+
|
|
14
|
+
const fetcher = (url: string) =>
|
|
15
|
+
fetch(url).then(r => { if (!r.ok) throw new Error(`API error ${r.status}`); return r.json() })
|
|
16
|
+
|
|
17
|
+
type SortKey = 'last_active' | 'estimated_cost' | 'session_count' | 'total_duration_minutes'
|
|
18
|
+
|
|
19
|
+
const SORT_OPTIONS: { k: SortKey; label: string }[] = [
|
|
20
|
+
{ k: 'last_active', label: 'Recent' },
|
|
21
|
+
{ k: 'estimated_cost', label: 'Cost' },
|
|
22
|
+
{ k: 'session_count', label: 'Sessions' },
|
|
23
|
+
{ k: 'total_duration_minutes', label: 'Time' },
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
export default function ProjectsPage() {
|
|
27
|
+
const { data, error, isLoading } = useSWR<{ projects: ProjectSummary[] }>(
|
|
28
|
+
'/api/projects', fetcher, { refreshInterval: 5_000 }
|
|
29
|
+
)
|
|
30
|
+
const [sort, setSort] = useState<SortKey>('last_active')
|
|
31
|
+
const [search, setSearch] = useState('')
|
|
32
|
+
|
|
33
|
+
const sorted = useMemo(() => {
|
|
34
|
+
if (!data) return []
|
|
35
|
+
let projects = [...data.projects]
|
|
36
|
+
if (search) {
|
|
37
|
+
const q = search.toLowerCase()
|
|
38
|
+
projects = projects.filter(p =>
|
|
39
|
+
p.display_name.toLowerCase().includes(q) ||
|
|
40
|
+
p.project_path.toLowerCase().includes(q)
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
return projects.sort((a, b) => {
|
|
44
|
+
if (sort === 'last_active') return b.last_active.localeCompare(a.last_active)
|
|
45
|
+
return (b[sort] as number) - (a[sort] as number)
|
|
46
|
+
})
|
|
47
|
+
}, [data, sort, search])
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div className="flex flex-col min-h-screen">
|
|
51
|
+
<TopBar
|
|
52
|
+
title="Projects"
|
|
53
|
+
subtitle={data ? `${data.projects.length} projects` : 'Loading…'}
|
|
54
|
+
/>
|
|
55
|
+
<div className="p-6 space-y-4">
|
|
56
|
+
|
|
57
|
+
{error && (
|
|
58
|
+
<Alert variant="destructive">
|
|
59
|
+
<AlertTriangle className="h-4 w-4" />
|
|
60
|
+
<AlertDescription>Error loading data: {String(error)}</AlertDescription>
|
|
61
|
+
</Alert>
|
|
62
|
+
)}
|
|
63
|
+
|
|
64
|
+
{/* Search + sort toolbar */}
|
|
65
|
+
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3">
|
|
66
|
+
<div className="relative flex-1 w-full sm:max-w-xs">
|
|
67
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
68
|
+
<Input
|
|
69
|
+
placeholder="Search projects…"
|
|
70
|
+
value={search}
|
|
71
|
+
onChange={e => setSearch(e.target.value)}
|
|
72
|
+
className="pl-9"
|
|
73
|
+
/>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="flex items-center gap-2 ml-auto">
|
|
76
|
+
<ArrowUpDown className="w-4 h-4 text-muted-foreground" />
|
|
77
|
+
<Select value={sort} onValueChange={v => setSort(v as SortKey)}>
|
|
78
|
+
<SelectTrigger className="w-36">
|
|
79
|
+
<SelectValue />
|
|
80
|
+
</SelectTrigger>
|
|
81
|
+
<SelectContent>
|
|
82
|
+
{SORT_OPTIONS.map(({ k, label }) => (
|
|
83
|
+
<SelectItem key={k} value={k}>{label}</SelectItem>
|
|
84
|
+
))}
|
|
85
|
+
</SelectContent>
|
|
86
|
+
</Select>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{isLoading && (
|
|
91
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4">
|
|
92
|
+
{Array.from({ length: 6 }).map((_, i) => <Skeleton key={i} className="h-48 rounded-xl" />)}
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
{sorted.length > 0 && (
|
|
97
|
+
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4">
|
|
98
|
+
{sorted.map(p => <ProjectCard key={p.slug} project={p} />)}
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
101
|
+
|
|
102
|
+
{!isLoading && sorted.length === 0 && (
|
|
103
|
+
<div className="text-center py-16 text-muted-foreground text-sm">
|
|
104
|
+
{search ? 'No projects match your search.' : 'No projects found in ~/.orca/'}
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}
|