@axplusb/kepler 0.0.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +98 -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,243 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { use } from 'react'
|
|
4
|
+
import useSWR from 'swr'
|
|
5
|
+
import { TopBar } from '@/components/layout/top-bar'
|
|
6
|
+
import { SessionSidebar } from '@/components/sessions/replay/session-sidebar'
|
|
7
|
+
import { UserTurnCard, AssistantTurnCard } from '@/components/sessions/replay/turn-cards'
|
|
8
|
+
import { TokenAccumulationChart } from '@/components/sessions/replay/token-accumulation-chart'
|
|
9
|
+
import { SessionBadges } from '@/components/sessions/session-badges'
|
|
10
|
+
import { formatCost, formatTokens, formatDuration, projectDisplayName } from '@/lib/decode'
|
|
11
|
+
import type { ReplayData, SessionWithFacet } from '@/types/claude'
|
|
12
|
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
13
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
14
|
+
import { Alert, AlertDescription } from '@/components/ui/alert'
|
|
15
|
+
import { AlertTriangle, MessageSquare, Coins, DollarSign, Clock, Zap } from 'lucide-react'
|
|
16
|
+
|
|
17
|
+
const fetcher = (url: string) =>
|
|
18
|
+
fetch(url).then(r => { if (!r.ok) throw new Error(`API error ${r.status}`); return r.json() })
|
|
19
|
+
|
|
20
|
+
type ReplayResponse = ReplayData
|
|
21
|
+
|
|
22
|
+
export default function SessionDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
|
23
|
+
const { id } = use(params)
|
|
24
|
+
|
|
25
|
+
const { data: replayData, error: replayError, isLoading: replayLoading } =
|
|
26
|
+
useSWR<ReplayResponse>(`/api/sessions/${id}/replay`, fetcher)
|
|
27
|
+
|
|
28
|
+
const { data: metaData } =
|
|
29
|
+
useSWR<{ session: SessionWithFacet }>(`/api/sessions/${id}`, fetcher)
|
|
30
|
+
|
|
31
|
+
const meta = metaData?.session
|
|
32
|
+
|
|
33
|
+
if (replayError) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex flex-col min-h-screen">
|
|
36
|
+
<TopBar title="Session Replay" subtitle="Error" />
|
|
37
|
+
<div className="p-6">
|
|
38
|
+
<Alert variant="destructive">
|
|
39
|
+
<AlertTriangle className="h-4 w-4" />
|
|
40
|
+
<AlertDescription>Error loading session: {String(replayError)}</AlertDescription>
|
|
41
|
+
</Alert>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (replayLoading || !replayData) {
|
|
48
|
+
return (
|
|
49
|
+
<div className="flex flex-col min-h-screen">
|
|
50
|
+
<TopBar title="Session Replay" subtitle="Loading…" />
|
|
51
|
+
<div className="p-6 space-y-4">
|
|
52
|
+
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
|
53
|
+
{Array.from({ length: 4 }).map((_, i) => <Skeleton key={i} className="h-20 rounded-xl" />)}
|
|
54
|
+
</div>
|
|
55
|
+
<div className="space-y-4">
|
|
56
|
+
{Array.from({ length: 5 }).map((_, i) => (
|
|
57
|
+
<Skeleton key={i} className={`h-${i % 2 === 0 ? '16' : '28'} rounded-xl`} />
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const replay = replayData
|
|
66
|
+
const projectName = meta ? projectDisplayName(meta.project_path ?? '') : id.slice(0, 8)
|
|
67
|
+
|
|
68
|
+
// Total token counts from replay
|
|
69
|
+
let totalInput = 0, totalOutput = 0, totalCacheRead = 0, totalCacheWrite = 0
|
|
70
|
+
for (const t of replay.turns) {
|
|
71
|
+
if (t.usage) {
|
|
72
|
+
totalInput += t.usage.input_tokens ?? 0
|
|
73
|
+
totalOutput += t.usage.output_tokens ?? 0
|
|
74
|
+
totalCacheWrite += t.usage.cache_creation_input_tokens ?? 0
|
|
75
|
+
totalCacheRead += t.usage.cache_read_input_tokens ?? 0
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const totalTokens = totalInput + totalOutput + totalCacheWrite + totalCacheRead
|
|
79
|
+
|
|
80
|
+
// Build tool results map: tool_use_id -> result (from user turns)
|
|
81
|
+
const toolResults = new Map<string, { content: string; is_error: boolean }>()
|
|
82
|
+
for (const t of replay.turns) {
|
|
83
|
+
if (t.type === 'user' && t.tool_results) {
|
|
84
|
+
for (const r of t.tool_results) {
|
|
85
|
+
toolResults.set(r.tool_use_id, { content: r.content, is_error: r.is_error })
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Build compaction map: index of turn before which a compaction occurred
|
|
91
|
+
const compactionByTurnIndex = new Map(replay.compactions.map(c => [c.turn_index, c]))
|
|
92
|
+
|
|
93
|
+
let assistantTurnNum = 0
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div className="flex flex-col min-h-screen">
|
|
97
|
+
{/* Header */}
|
|
98
|
+
<TopBar
|
|
99
|
+
title={`${projectName} · ${replay.slug ?? id.slice(0, 8)}`}
|
|
100
|
+
subtitle={`${replay.git_branch ?? '?'} · v${replay.version ?? '?'} · ${formatCost(replay.total_cost ?? 0)}`}
|
|
101
|
+
/>
|
|
102
|
+
|
|
103
|
+
{/* Stats cards — match project detail page */}
|
|
104
|
+
<div className="border-b border-border bg-muted/30 px-4 py-4 md:px-6">
|
|
105
|
+
<div
|
|
106
|
+
className={
|
|
107
|
+
3 + (meta ? 1 : 0) + (replay.compactions.length > 0 ? 1 : 0) >= 5
|
|
108
|
+
? 'grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-5'
|
|
109
|
+
: 'grid grid-cols-2 gap-4 sm:grid-cols-4'
|
|
110
|
+
}
|
|
111
|
+
>
|
|
112
|
+
<Card className="gap-0">
|
|
113
|
+
<CardHeader className="pb-2">
|
|
114
|
+
<CardDescription className="flex items-center gap-2">
|
|
115
|
+
<MessageSquare className="h-4 w-4" /> Turns
|
|
116
|
+
</CardDescription>
|
|
117
|
+
<CardTitle className="text-3xl font-bold tabular-nums">
|
|
118
|
+
{replay.turns.filter(t => t.type === 'assistant').length}
|
|
119
|
+
</CardTitle>
|
|
120
|
+
</CardHeader>
|
|
121
|
+
<CardContent>
|
|
122
|
+
<p className="text-xs text-muted-foreground">Assistant messages</p>
|
|
123
|
+
</CardContent>
|
|
124
|
+
</Card>
|
|
125
|
+
|
|
126
|
+
<Card className="gap-0">
|
|
127
|
+
<CardHeader className="pb-2">
|
|
128
|
+
<CardDescription className="flex items-center gap-2">
|
|
129
|
+
<Coins className="h-4 w-4" /> Tokens
|
|
130
|
+
</CardDescription>
|
|
131
|
+
<CardTitle className="text-3xl font-bold tabular-nums text-blue-700 dark:text-[#60a5fa]">{formatTokens(totalTokens)}</CardTitle>
|
|
132
|
+
</CardHeader>
|
|
133
|
+
<CardContent>
|
|
134
|
+
<p className="text-xs text-muted-foreground">Input + output + cache</p>
|
|
135
|
+
</CardContent>
|
|
136
|
+
</Card>
|
|
137
|
+
|
|
138
|
+
<Card className="gap-0">
|
|
139
|
+
<CardHeader className="pb-2">
|
|
140
|
+
<CardDescription className="flex items-center gap-2">
|
|
141
|
+
<DollarSign className="h-4 w-4" /> Cost
|
|
142
|
+
</CardDescription>
|
|
143
|
+
<CardTitle className="text-3xl font-bold tabular-nums text-[#d97706]">
|
|
144
|
+
{formatCost(replay.total_cost ?? 0)}
|
|
145
|
+
</CardTitle>
|
|
146
|
+
</CardHeader>
|
|
147
|
+
<CardContent>
|
|
148
|
+
<p className="text-xs text-muted-foreground">Estimated spend</p>
|
|
149
|
+
</CardContent>
|
|
150
|
+
</Card>
|
|
151
|
+
|
|
152
|
+
{meta && (
|
|
153
|
+
<Card className="gap-0">
|
|
154
|
+
<CardHeader className="pb-2">
|
|
155
|
+
<CardDescription className="flex items-center gap-2">
|
|
156
|
+
<Clock className="h-4 w-4" /> Duration
|
|
157
|
+
</CardDescription>
|
|
158
|
+
<CardTitle className="text-3xl font-bold tabular-nums">
|
|
159
|
+
{formatDuration(meta.duration_minutes ?? 0)}
|
|
160
|
+
</CardTitle>
|
|
161
|
+
</CardHeader>
|
|
162
|
+
<CardContent>
|
|
163
|
+
<p className="text-xs text-muted-foreground">Session span</p>
|
|
164
|
+
</CardContent>
|
|
165
|
+
</Card>
|
|
166
|
+
)}
|
|
167
|
+
|
|
168
|
+
{replay.compactions.length > 0 && (
|
|
169
|
+
<Card className="gap-0 border-amber-500/25">
|
|
170
|
+
<CardHeader className="pb-2">
|
|
171
|
+
<CardDescription className="flex items-center gap-2">
|
|
172
|
+
<Zap className="h-4 w-4 text-amber-500" /> Compactions
|
|
173
|
+
</CardDescription>
|
|
174
|
+
<CardTitle className="text-3xl font-bold tabular-nums text-amber-500">
|
|
175
|
+
{replay.compactions.length}
|
|
176
|
+
</CardTitle>
|
|
177
|
+
</CardHeader>
|
|
178
|
+
<CardContent>
|
|
179
|
+
<p className="text-xs text-muted-foreground">Context window events</p>
|
|
180
|
+
</CardContent>
|
|
181
|
+
</Card>
|
|
182
|
+
)}
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
{meta && (
|
|
186
|
+
<div className="mt-4 flex flex-wrap gap-2">
|
|
187
|
+
<SessionBadges
|
|
188
|
+
has_compaction={replay.compactions.length > 0}
|
|
189
|
+
uses_task_agent={meta.uses_task_agent}
|
|
190
|
+
uses_mcp={meta.uses_mcp}
|
|
191
|
+
uses_web_search={meta.uses_web_search}
|
|
192
|
+
uses_web_fetch={meta.uses_web_fetch}
|
|
193
|
+
has_thinking={meta.has_thinking}
|
|
194
|
+
/>
|
|
195
|
+
</div>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{/* Two-column layout */}
|
|
200
|
+
<div className="flex flex-1 overflow-hidden">
|
|
201
|
+
{/* Conversation replay */}
|
|
202
|
+
<div className="flex-1 min-w-0 overflow-y-auto px-4 py-6 max-w-6xl">
|
|
203
|
+
{replay.turns.map((turn, i) => {
|
|
204
|
+
const compactionBefore = compactionByTurnIndex.get(i)
|
|
205
|
+
|
|
206
|
+
if (turn.type === 'user') {
|
|
207
|
+
return (
|
|
208
|
+
<UserTurnCard
|
|
209
|
+
key={turn.uuid || i}
|
|
210
|
+
turn={turn}
|
|
211
|
+
turnNumber={i + 1}
|
|
212
|
+
compactionBefore={compactionBefore}
|
|
213
|
+
toolResults={toolResults}
|
|
214
|
+
/>
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
assistantTurnNum++
|
|
219
|
+
return (
|
|
220
|
+
<AssistantTurnCard
|
|
221
|
+
key={turn.uuid || i}
|
|
222
|
+
turn={turn}
|
|
223
|
+
turnNumber={assistantTurnNum}
|
|
224
|
+
compactionBefore={compactionBefore}
|
|
225
|
+
toolResults={toolResults}
|
|
226
|
+
/>
|
|
227
|
+
)
|
|
228
|
+
})}
|
|
229
|
+
</div>
|
|
230
|
+
|
|
231
|
+
{/* Sidebar */}
|
|
232
|
+
<div className="w-64 shrink-0 overflow-y-auto border-l border-border px-4 py-6">
|
|
233
|
+
<SessionSidebar replay={replay} meta={meta} />
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
{/* Token accumulation chart */}
|
|
238
|
+
<div className="border-t border-border px-4 py-4">
|
|
239
|
+
<TokenAccumulationChart turns={replay.turns} compactions={replay.compactions} />
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
)
|
|
243
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import useSWR from 'swr'
|
|
4
|
+
import { TopBar } from '@/components/layout/top-bar'
|
|
5
|
+
import { SessionTable } from '@/components/sessions/session-table'
|
|
6
|
+
import type { SessionWithFacet } from '@/types/claude'
|
|
7
|
+
|
|
8
|
+
const fetcher = (url: string) =>
|
|
9
|
+
fetch(url).then(r => { if (!r.ok) throw new Error(`API error ${r.status}`); return r.json() })
|
|
10
|
+
|
|
11
|
+
export default function SessionsPage() {
|
|
12
|
+
const { data, error, isLoading } = useSWR<{ sessions: SessionWithFacet[]; total: number }>(
|
|
13
|
+
'/api/sessions',
|
|
14
|
+
fetcher,
|
|
15
|
+
{ refreshInterval: 5_000 }
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div className="flex flex-col min-h-screen">
|
|
20
|
+
<TopBar
|
|
21
|
+
title="Orca Pulse · Sessions"
|
|
22
|
+
subtitle={data ? `${data.total} total sessions` : 'loading...'}
|
|
23
|
+
/>
|
|
24
|
+
<div className="p-6">
|
|
25
|
+
{error && (
|
|
26
|
+
<p className="text-[#f87171] text-sm font-mono">Error: {String(error)}</p>
|
|
27
|
+
)}
|
|
28
|
+
{isLoading && (
|
|
29
|
+
<div className="space-y-2">
|
|
30
|
+
{Array.from({ length: 8 }).map((_, i) => (
|
|
31
|
+
<div key={i} className="h-10 bg-muted rounded animate-pulse" />
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
{data && <SessionTable sessions={data.sessions} />}
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import useSWR from 'swr'
|
|
4
|
+
import { TopBar } from '@/components/layout/top-bar'
|
|
5
|
+
import type { SkillInfo, PluginInfo } from '@/lib/claude-reader'
|
|
6
|
+
|
|
7
|
+
const fetcher = (url: string) =>
|
|
8
|
+
fetch(url).then(r => { if (!r.ok) throw new Error(`API error ${r.status}`); return r.json() })
|
|
9
|
+
|
|
10
|
+
function formatBytes(b: number) {
|
|
11
|
+
if (b >= 1_073_741_824) return (b / 1_073_741_824).toFixed(2) + ' GB'
|
|
12
|
+
if (b >= 1_048_576) return (b / 1_048_576).toFixed(1) + ' MB'
|
|
13
|
+
if (b >= 1_024) return (b / 1_024).toFixed(1) + ' KB'
|
|
14
|
+
return b + ' B'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function JsonValue({ value, depth = 0 }: { value: unknown; depth?: number }) {
|
|
18
|
+
if (value === null) return <span className="text-muted-foreground">null</span>
|
|
19
|
+
if (typeof value === 'boolean')
|
|
20
|
+
return <span className="text-amber-700 dark:text-[#fbbf24]">{String(value)}</span>
|
|
21
|
+
if (typeof value === 'number')
|
|
22
|
+
return <span className="text-emerald-700 dark:text-[#6ee7b7]">{value}</span>
|
|
23
|
+
if (typeof value === 'string')
|
|
24
|
+
return <span className="text-orange-400 dark:text-[#f9a875]">"{value}"</span>
|
|
25
|
+
if (Array.isArray(value)) {
|
|
26
|
+
if (value.length === 0) return <span className="text-muted-foreground">[]</span>
|
|
27
|
+
return (
|
|
28
|
+
<span>
|
|
29
|
+
<span className="text-muted-foreground">[</span>
|
|
30
|
+
<div className="pl-4">
|
|
31
|
+
{value.map((v, i) => (
|
|
32
|
+
<div key={i}>
|
|
33
|
+
<JsonValue value={v} depth={depth + 1} />
|
|
34
|
+
{i < value.length - 1 && <span className="text-muted-foreground/60">,</span>}
|
|
35
|
+
</div>
|
|
36
|
+
))}
|
|
37
|
+
</div>
|
|
38
|
+
<span className="text-muted-foreground">]</span>
|
|
39
|
+
</span>
|
|
40
|
+
)
|
|
41
|
+
}
|
|
42
|
+
if (typeof value === 'object') {
|
|
43
|
+
const entries = Object.entries(value as Record<string, unknown>)
|
|
44
|
+
if (entries.length === 0) return <span className="text-muted-foreground">{'{}'}</span>
|
|
45
|
+
return (
|
|
46
|
+
<span>
|
|
47
|
+
<span className="text-muted-foreground">{'{'}</span>
|
|
48
|
+
<div className="pl-4">
|
|
49
|
+
{entries.map(([k, v], i) => (
|
|
50
|
+
<div key={k}>
|
|
51
|
+
<span className="text-muted-foreground">"</span>
|
|
52
|
+
<span className="text-blue-700 dark:text-[#93c5fd]">{k}</span>
|
|
53
|
+
<span className="text-muted-foreground">"</span>
|
|
54
|
+
<span className="text-muted-foreground/60">: </span>
|
|
55
|
+
<JsonValue value={v} depth={depth + 1} />
|
|
56
|
+
{i < entries.length - 1 && <span className="text-muted-foreground/60">,</span>}
|
|
57
|
+
</div>
|
|
58
|
+
))}
|
|
59
|
+
</div>
|
|
60
|
+
<span className="text-muted-foreground">{'}'}</span>
|
|
61
|
+
</span>
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
return <span className="text-foreground">{String(value)}</span>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
|
68
|
+
return (
|
|
69
|
+
<div className="border border-border rounded bg-card p-4">
|
|
70
|
+
<h2 className="text-[13px] font-bold text-muted-foreground uppercase tracking-widest mb-4">{title}</h2>
|
|
71
|
+
{children}
|
|
72
|
+
</div>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default function SettingsPage() {
|
|
77
|
+
const { data, error, isLoading } = useSWR<{
|
|
78
|
+
settings: Record<string, unknown>
|
|
79
|
+
storageBytes: number
|
|
80
|
+
skills: SkillInfo[]
|
|
81
|
+
plugins: PluginInfo[]
|
|
82
|
+
}>('/api/settings', fetcher, { refreshInterval: 30_000 })
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="flex flex-col min-h-screen">
|
|
86
|
+
<TopBar title="orca-pulse · settings" subtitle="~/.orca/settings.json" />
|
|
87
|
+
<div className="p-4 md:p-6 space-y-6">
|
|
88
|
+
{error && <p className="text-[#f87171] text-sm font-mono">Error: {String(error)}</p>}
|
|
89
|
+
{isLoading && (
|
|
90
|
+
<div className="space-y-4">
|
|
91
|
+
{Array.from({ length: 3 }).map((_, i) => (
|
|
92
|
+
<div key={i} className="h-32 bg-muted rounded animate-pulse" />
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
{data && (
|
|
97
|
+
<>
|
|
98
|
+
<Section title="Storage">
|
|
99
|
+
<div className="flex items-center gap-3">
|
|
100
|
+
<span className="text-primary text-2xl font-mono font-bold">
|
|
101
|
+
{formatBytes(data.storageBytes)}
|
|
102
|
+
</span>
|
|
103
|
+
<span className="text-muted-foreground text-sm font-mono">used by ~/.orca/</span>
|
|
104
|
+
</div>
|
|
105
|
+
</Section>
|
|
106
|
+
|
|
107
|
+
<Section title="Settings">
|
|
108
|
+
{Object.keys(data.settings).length === 0 ? (
|
|
109
|
+
<p className="text-muted-foreground/60 text-sm font-mono">No settings found in ~/.orca/settings.json</p>
|
|
110
|
+
) : (
|
|
111
|
+
<div className="font-mono text-sm leading-relaxed overflow-x-auto">
|
|
112
|
+
<JsonValue value={data.settings} />
|
|
113
|
+
</div>
|
|
114
|
+
)}
|
|
115
|
+
</Section>
|
|
116
|
+
|
|
117
|
+
{data.settings.env && (
|
|
118
|
+
<Section title="Environment Variables">
|
|
119
|
+
<div className="font-mono text-sm leading-relaxed overflow-x-auto">
|
|
120
|
+
<JsonValue value={data.settings.env} />
|
|
121
|
+
</div>
|
|
122
|
+
</Section>
|
|
123
|
+
)}
|
|
124
|
+
|
|
125
|
+
{data.settings.mcpServers && (
|
|
126
|
+
<Section title="MCP Servers">
|
|
127
|
+
<div className="space-y-3">
|
|
128
|
+
{Object.entries(data.settings.mcpServers as Record<string, unknown>).map(([name, cfg]) => (
|
|
129
|
+
<div key={name} className="border border-border rounded p-3">
|
|
130
|
+
<p className="text-primary font-mono text-sm font-bold mb-2">{name}</p>
|
|
131
|
+
<div className="font-mono text-xs text-muted-foreground overflow-x-auto">
|
|
132
|
+
<JsonValue value={cfg} />
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
))}
|
|
136
|
+
</div>
|
|
137
|
+
</Section>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
<Section title={`Skills (${data.skills.length})`}>
|
|
141
|
+
{data.skills.length === 0 ? (
|
|
142
|
+
<p className="text-muted-foreground/60 text-sm font-mono">No skills found in ~/.orca/skills/</p>
|
|
143
|
+
) : (
|
|
144
|
+
<div className="grid gap-2">
|
|
145
|
+
{data.skills.map(skill => (
|
|
146
|
+
<div key={skill.name} className="border border-border rounded p-3 flex items-start gap-3">
|
|
147
|
+
<span className="shrink-0 w-2 h-2 mt-1.5 rounded-full bg-primary" />
|
|
148
|
+
<div className="min-w-0">
|
|
149
|
+
<p className="text-primary font-mono text-sm font-bold">{skill.name}</p>
|
|
150
|
+
{skill.description && (
|
|
151
|
+
<p className="text-foreground text-xs mt-0.5">{skill.description}</p>
|
|
152
|
+
)}
|
|
153
|
+
{skill.triggers && (
|
|
154
|
+
<p className="text-muted-foreground text-xs mt-1 leading-relaxed line-clamp-2">{skill.triggers}</p>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
))}
|
|
159
|
+
</div>
|
|
160
|
+
)}
|
|
161
|
+
</Section>
|
|
162
|
+
|
|
163
|
+
{data.plugins.length > 0 && (
|
|
164
|
+
<Section title={`Plugins (${data.plugins.length})`}>
|
|
165
|
+
<div className="grid gap-2">
|
|
166
|
+
{data.plugins.map((plugin, i) => (
|
|
167
|
+
<div key={i} className="border border-border rounded p-3 flex items-center justify-between gap-4">
|
|
168
|
+
<div className="min-w-0">
|
|
169
|
+
<p className="text-primary font-mono text-sm font-bold">{plugin.id}</p>
|
|
170
|
+
<p className="text-muted-foreground text-xs mt-0.5">scope: {plugin.scope}</p>
|
|
171
|
+
</div>
|
|
172
|
+
<div className="text-right shrink-0">
|
|
173
|
+
<span className="text-emerald-700 dark:text-[#6ee7b7] font-mono text-xs">v{plugin.version}</span>
|
|
174
|
+
<p className="text-muted-foreground text-xs mt-0.5">
|
|
175
|
+
{new Date(plugin.installedAt).toLocaleDateString()}
|
|
176
|
+
</p>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
))}
|
|
180
|
+
</div>
|
|
181
|
+
</Section>
|
|
182
|
+
)}
|
|
183
|
+
</>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import useSWR from 'swr'
|
|
5
|
+
import { TopBar } from '@/components/layout/top-bar'
|
|
6
|
+
import { Input } from '@/components/ui/input'
|
|
7
|
+
import { Badge } from '@/components/ui/badge'
|
|
8
|
+
import { Card, CardContent } from '@/components/ui/card'
|
|
9
|
+
import { Skeleton } from '@/components/ui/skeleton'
|
|
10
|
+
import { Alert, AlertDescription } from '@/components/ui/alert'
|
|
11
|
+
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
12
|
+
import { Search, AlertTriangle, Circle, CircleDot, CircleCheck } 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
|
+
interface TodoItem {
|
|
18
|
+
id?: string
|
|
19
|
+
content?: string
|
|
20
|
+
status?: string
|
|
21
|
+
priority?: string
|
|
22
|
+
[key: string]: unknown
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TodoFile {
|
|
26
|
+
name: string
|
|
27
|
+
data: unknown
|
|
28
|
+
mtime: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type FilterType = 'all' | 'pending' | 'in_progress' | 'completed'
|
|
32
|
+
|
|
33
|
+
function parseTodos(data: unknown): TodoItem[] {
|
|
34
|
+
if (Array.isArray(data)) return data as TodoItem[]
|
|
35
|
+
if (data && typeof data === 'object' && 'todos' in data)
|
|
36
|
+
return parseTodos((data as { todos: unknown }).todos)
|
|
37
|
+
return []
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function formatDate(iso: string) {
|
|
41
|
+
return new Date(iso).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeStatus(s?: string): string {
|
|
45
|
+
return s ?? 'pending'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const PRIORITY_STYLES: Record<string, string> = {
|
|
49
|
+
high: 'bg-red-500/10 text-red-400 border-red-500/30',
|
|
50
|
+
medium: 'bg-amber-500/10 text-amber-400 border-amber-500/30',
|
|
51
|
+
low: 'bg-muted text-muted-foreground border-border',
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const STATUS_META: Record<string, { icon: React.ElementType; color: string; label: string }> = {
|
|
55
|
+
pending: { icon: Circle, color: 'text-muted-foreground', label: 'Pending' },
|
|
56
|
+
in_progress: { icon: CircleDot, color: 'text-amber-400', label: 'In Progress' },
|
|
57
|
+
completed: { icon: CircleCheck, color: 'text-emerald-400', label: 'Completed' },
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function TodoRow({ item, file }: { item: TodoItem; file: TodoFile }) {
|
|
61
|
+
const status = normalizeStatus(item.status)
|
|
62
|
+
const meta = STATUS_META[status] ?? STATUS_META.pending
|
|
63
|
+
const Icon = meta.icon
|
|
64
|
+
const isCompleted = status === 'completed'
|
|
65
|
+
|
|
66
|
+
return (
|
|
67
|
+
<Card className="py-0">
|
|
68
|
+
<CardContent className="px-4 py-3.5 flex items-start gap-3">
|
|
69
|
+
<Icon className={`w-4 h-4 mt-0.5 shrink-0 ${meta.color}`} />
|
|
70
|
+
<div className="flex-1 min-w-0">
|
|
71
|
+
<p className={`text-sm leading-relaxed ${isCompleted ? 'line-through text-muted-foreground/50' : 'text-foreground'}`}>
|
|
72
|
+
{String(item.content ?? JSON.stringify(item))}
|
|
73
|
+
</p>
|
|
74
|
+
<div className="flex flex-wrap items-center gap-2 mt-2">
|
|
75
|
+
<Badge
|
|
76
|
+
variant="outline"
|
|
77
|
+
className={`text-[10px] font-mono uppercase tracking-wider ${meta.color}`}
|
|
78
|
+
>
|
|
79
|
+
{meta.label}
|
|
80
|
+
</Badge>
|
|
81
|
+
{item.priority && (
|
|
82
|
+
<Badge
|
|
83
|
+
variant="outline"
|
|
84
|
+
className={`text-[10px] font-mono uppercase tracking-wider ${PRIORITY_STYLES[item.priority] ?? PRIORITY_STYLES.low}`}
|
|
85
|
+
>
|
|
86
|
+
{item.priority}
|
|
87
|
+
</Badge>
|
|
88
|
+
)}
|
|
89
|
+
<Badge variant="secondary" className="text-[10px] font-mono">
|
|
90
|
+
{file.name}
|
|
91
|
+
</Badge>
|
|
92
|
+
<span className="text-[10px] text-muted-foreground/60">
|
|
93
|
+
{formatDate(file.mtime)}
|
|
94
|
+
</span>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</CardContent>
|
|
98
|
+
</Card>
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export default function TodosPage() {
|
|
103
|
+
const { data, error, isLoading } = useSWR<{ todos: TodoFile[] }>(
|
|
104
|
+
'/api/todos', fetcher, { refreshInterval: 10_000 }
|
|
105
|
+
)
|
|
106
|
+
const [search, setSearch] = useState('')
|
|
107
|
+
const [filter, setFilter] = useState<FilterType>('all')
|
|
108
|
+
|
|
109
|
+
const todos = data?.todos ?? []
|
|
110
|
+
const allItems: Array<{ file: TodoFile; item: TodoItem }> = todos.flatMap(file =>
|
|
111
|
+
parseTodos(file.data).map(item => ({ file, item }))
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
const counts = {
|
|
115
|
+
all: allItems.length,
|
|
116
|
+
pending: allItems.filter(x => normalizeStatus(x.item.status) === 'pending').length,
|
|
117
|
+
in_progress: allItems.filter(x => normalizeStatus(x.item.status) === 'in_progress').length,
|
|
118
|
+
completed: allItems.filter(x => normalizeStatus(x.item.status) === 'completed').length,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const filtered = allItems.filter(({ item }) => {
|
|
122
|
+
if (filter !== 'all' && normalizeStatus(item.status) !== filter) return false
|
|
123
|
+
if (search && !String(item.content ?? '').toLowerCase().includes(search.toLowerCase())) return false
|
|
124
|
+
return true
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<div className="flex flex-col min-h-screen">
|
|
129
|
+
<TopBar title="Todos" subtitle="~/.orca/todos/" />
|
|
130
|
+
<div className="p-4 md:p-6 space-y-5">
|
|
131
|
+
|
|
132
|
+
{error && (
|
|
133
|
+
<Alert variant="destructive">
|
|
134
|
+
<AlertTriangle className="h-4 w-4" />
|
|
135
|
+
<AlertDescription>Error loading data: {String(error)}</AlertDescription>
|
|
136
|
+
</Alert>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
{isLoading && (
|
|
140
|
+
<div className="space-y-3">
|
|
141
|
+
<Skeleton className="h-10 w-full rounded-lg" />
|
|
142
|
+
{Array.from({ length: 5 }).map((_, i) => <Skeleton key={i} className="h-16 rounded-xl" />)}
|
|
143
|
+
</div>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{data && (
|
|
147
|
+
<>
|
|
148
|
+
{/* Filter tabs */}
|
|
149
|
+
<Tabs value={filter} onValueChange={v => setFilter(v as FilterType)}>
|
|
150
|
+
<TabsList className="w-full sm:w-auto">
|
|
151
|
+
<TabsTrigger value="all" className="gap-2">
|
|
152
|
+
All
|
|
153
|
+
<Badge variant="secondary" className="text-xs tabular-nums">{counts.all}</Badge>
|
|
154
|
+
</TabsTrigger>
|
|
155
|
+
<TabsTrigger value="pending" className="gap-2">
|
|
156
|
+
<Circle className="w-3.5 h-3.5" />
|
|
157
|
+
Pending
|
|
158
|
+
<Badge variant="secondary" className="text-xs tabular-nums">{counts.pending}</Badge>
|
|
159
|
+
</TabsTrigger>
|
|
160
|
+
<TabsTrigger value="in_progress" className="gap-2">
|
|
161
|
+
<CircleDot className="w-3.5 h-3.5 text-amber-400" />
|
|
162
|
+
In Progress
|
|
163
|
+
<Badge variant="secondary" className="text-xs tabular-nums">{counts.in_progress}</Badge>
|
|
164
|
+
</TabsTrigger>
|
|
165
|
+
<TabsTrigger value="completed" className="gap-2">
|
|
166
|
+
<CircleCheck className="w-3.5 h-3.5 text-emerald-400" />
|
|
167
|
+
Done
|
|
168
|
+
<Badge variant="secondary" className="text-xs tabular-nums">{counts.completed}</Badge>
|
|
169
|
+
</TabsTrigger>
|
|
170
|
+
</TabsList>
|
|
171
|
+
</Tabs>
|
|
172
|
+
|
|
173
|
+
{/* Search */}
|
|
174
|
+
<div className="relative">
|
|
175
|
+
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
|
176
|
+
<Input
|
|
177
|
+
placeholder="Search todos…"
|
|
178
|
+
value={search}
|
|
179
|
+
onChange={e => setSearch(e.target.value)}
|
|
180
|
+
className="pl-9"
|
|
181
|
+
/>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
{(search || filter !== 'all') && (
|
|
185
|
+
<p className="text-xs text-muted-foreground">
|
|
186
|
+
Showing <span className="text-amber-400 font-medium">{filtered.length}</span> of {allItems.length} todos
|
|
187
|
+
</p>
|
|
188
|
+
)}
|
|
189
|
+
|
|
190
|
+
{filtered.length === 0 ? (
|
|
191
|
+
<div className="text-center py-16">
|
|
192
|
+
<CircleCheck className="w-8 h-8 mx-auto mb-3 text-muted-foreground/40" />
|
|
193
|
+
<p className="text-muted-foreground text-sm">
|
|
194
|
+
{allItems.length === 0
|
|
195
|
+
? 'No todos found in ~/.orca/todos/'
|
|
196
|
+
: 'No todos match your filter.'}
|
|
197
|
+
</p>
|
|
198
|
+
</div>
|
|
199
|
+
) : (
|
|
200
|
+
<div className="space-y-2">
|
|
201
|
+
{filtered.map(({ file, item }, i) => (
|
|
202
|
+
<TodoRow key={i} item={item} file={file} />
|
|
203
|
+
))}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
)
|
|
211
|
+
}
|