@axplusb/kepler 0.0.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/README.md +82 -0
  2. package/package.json +36 -4
  3. package/pulse/app/activity/page.tsx +190 -0
  4. package/pulse/app/api/activity/route.ts +138 -0
  5. package/pulse/app/api/costs/route.ts +88 -0
  6. package/pulse/app/api/export/route.ts +77 -0
  7. package/pulse/app/api/history/route.ts +11 -0
  8. package/pulse/app/api/import/route.ts +31 -0
  9. package/pulse/app/api/memory/route.ts +52 -0
  10. package/pulse/app/api/plans/route.ts +9 -0
  11. package/pulse/app/api/projects/[slug]/route.ts +96 -0
  12. package/pulse/app/api/projects/route.ts +121 -0
  13. package/pulse/app/api/sessions/[id]/replay/route.ts +20 -0
  14. package/pulse/app/api/sessions/[id]/route.ts +31 -0
  15. package/pulse/app/api/sessions/route.ts +112 -0
  16. package/pulse/app/api/settings/route.ts +14 -0
  17. package/pulse/app/api/stats/route.ts +143 -0
  18. package/pulse/app/api/todos/route.ts +9 -0
  19. package/pulse/app/api/tools/route.ts +160 -0
  20. package/pulse/app/costs/page.tsx +179 -0
  21. package/pulse/app/export/page.tsx +465 -0
  22. package/pulse/app/favicon.ico +0 -0
  23. package/pulse/app/globals.css +263 -0
  24. package/pulse/app/help/page.tsx +142 -0
  25. package/pulse/app/history/page.tsx +157 -0
  26. package/pulse/app/layout.tsx +46 -0
  27. package/pulse/app/memory/page.tsx +365 -0
  28. package/pulse/app/overview-client.tsx +393 -0
  29. package/pulse/app/page.tsx +14 -0
  30. package/pulse/app/plans/page.tsx +308 -0
  31. package/pulse/app/projects/[slug]/page.tsx +390 -0
  32. package/pulse/app/projects/page.tsx +110 -0
  33. package/pulse/app/sessions/[id]/page.tsx +243 -0
  34. package/pulse/app/sessions/page.tsx +39 -0
  35. package/pulse/app/settings/page.tsx +188 -0
  36. package/pulse/app/todos/page.tsx +211 -0
  37. package/pulse/app/tools/page.tsx +249 -0
  38. package/pulse/cli.js +159 -0
  39. package/pulse/components/activity/day-of-week-chart.tsx +35 -0
  40. package/pulse/components/activity/streak-card.tsx +36 -0
  41. package/pulse/components/costs/cache-efficiency-panel.tsx +76 -0
  42. package/pulse/components/costs/cost-by-project-chart.tsx +48 -0
  43. package/pulse/components/costs/cost-over-time-chart.tsx +95 -0
  44. package/pulse/components/costs/model-token-table.tsx +60 -0
  45. package/pulse/components/global-search.tsx +193 -0
  46. package/pulse/components/keyboard-nav-provider.tsx +23 -0
  47. package/pulse/components/layout/bottom-nav.tsx +52 -0
  48. package/pulse/components/layout/client-layout.tsx +31 -0
  49. package/pulse/components/layout/sidebar-context.tsx +50 -0
  50. package/pulse/components/layout/sidebar.tsx +182 -0
  51. package/pulse/components/layout/top-bar.tsx +121 -0
  52. package/pulse/components/overview/activity-heatmap.tsx +107 -0
  53. package/pulse/components/overview/conversation-table.tsx +148 -0
  54. package/pulse/components/overview/model-breakdown-donut.tsx +95 -0
  55. package/pulse/components/overview/peak-hours-chart.tsx +87 -0
  56. package/pulse/components/overview/project-activity-donut.tsx +96 -0
  57. package/pulse/components/overview/stat-card.tsx +102 -0
  58. package/pulse/components/overview/usage-over-time-chart.tsx +166 -0
  59. package/pulse/components/projects/project-card.tsx +175 -0
  60. package/pulse/components/sessions/replay/assistant-markdown.tsx +94 -0
  61. package/pulse/components/sessions/replay/compaction-card.tsx +25 -0
  62. package/pulse/components/sessions/replay/session-sidebar.tsx +231 -0
  63. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +98 -0
  64. package/pulse/components/sessions/replay/tool-call-badge.tsx +127 -0
  65. package/pulse/components/sessions/replay/turn-cards.tsx +220 -0
  66. package/pulse/components/sessions/replay/user-tool-result.tsx +158 -0
  67. package/pulse/components/sessions/session-badges.tsx +49 -0
  68. package/pulse/components/sessions/session-table.tsx +299 -0
  69. package/pulse/components/theme-provider.tsx +44 -0
  70. package/pulse/components/tools/feature-adoption-table.tsx +58 -0
  71. package/pulse/components/tools/mcp-server-panel.tsx +45 -0
  72. package/pulse/components/tools/tool-ranking-chart.tsx +57 -0
  73. package/pulse/components/tools/version-history-table.tsx +32 -0
  74. package/pulse/components/ui/alert.tsx +66 -0
  75. package/pulse/components/ui/badge.tsx +48 -0
  76. package/pulse/components/ui/breadcrumb.tsx +109 -0
  77. package/pulse/components/ui/button.tsx +64 -0
  78. package/pulse/components/ui/calendar.tsx +220 -0
  79. package/pulse/components/ui/card.tsx +92 -0
  80. package/pulse/components/ui/command.tsx +158 -0
  81. package/pulse/components/ui/dialog.tsx +158 -0
  82. package/pulse/components/ui/input.tsx +21 -0
  83. package/pulse/components/ui/popover.tsx +89 -0
  84. package/pulse/components/ui/progress.tsx +31 -0
  85. package/pulse/components/ui/select.tsx +190 -0
  86. package/pulse/components/ui/separator.tsx +28 -0
  87. package/pulse/components/ui/sheet.tsx +143 -0
  88. package/pulse/components/ui/skeleton.tsx +13 -0
  89. package/pulse/components/ui/table.tsx +116 -0
  90. package/pulse/components/ui/tabs.tsx +91 -0
  91. package/pulse/components/ui/tooltip.tsx +57 -0
  92. package/pulse/components/use-global-keyboard-nav.ts +79 -0
  93. package/pulse/components.json +23 -0
  94. package/pulse/eslint.config.mjs +18 -0
  95. package/pulse/lib/claude-reader.ts +594 -0
  96. package/pulse/lib/decode.ts +129 -0
  97. package/pulse/lib/pricing.ts +102 -0
  98. package/pulse/lib/replay-parser.ts +165 -0
  99. package/pulse/lib/tool-categories.ts +127 -0
  100. package/pulse/lib/utils.ts +6 -0
  101. package/pulse/next-env.d.ts +6 -0
  102. package/pulse/next.config.ts +16 -0
  103. package/pulse/package.json +45 -0
  104. package/pulse/postcss.config.mjs +7 -0
  105. package/pulse/public/activity.png +0 -0
  106. package/pulse/public/cc-lens.png +0 -0
  107. package/pulse/public/command-k.png +0 -0
  108. package/pulse/public/costs.png +0 -0
  109. package/pulse/public/dashboard-dark.png +0 -0
  110. package/pulse/public/dashboard-white.png +0 -0
  111. package/pulse/public/export.png +0 -0
  112. package/pulse/public/file.svg +1 -0
  113. package/pulse/public/globe.svg +1 -0
  114. package/pulse/public/next.svg +1 -0
  115. package/pulse/public/projects.png +0 -0
  116. package/pulse/public/session-chat.png +0 -0
  117. package/pulse/public/todos.png +0 -0
  118. package/pulse/public/tools.png +0 -0
  119. package/pulse/public/vercel.svg +1 -0
  120. package/pulse/public/window.svg +1 -0
  121. package/pulse/tsconfig.json +34 -0
  122. package/pulse/types/claude.ts +294 -0
  123. package/src/agents/loader.mjs +89 -0
  124. package/src/agents/parser.mjs +98 -0
  125. package/src/agents/teams.mjs +123 -0
  126. package/src/auth/oauth.mjs +220 -0
  127. package/src/auth/tarang-auth.mjs +277 -0
  128. package/src/config/cli-args.mjs +173 -0
  129. package/src/config/env.mjs +263 -0
  130. package/src/config/settings.mjs +132 -0
  131. package/src/context/ast-parser.mjs +298 -0
  132. package/src/context/bm25.mjs +85 -0
  133. package/src/context/retriever.mjs +270 -0
  134. package/src/context/skeleton.mjs +134 -0
  135. package/src/core/agent-loop.mjs +480 -0
  136. package/src/core/approval.mjs +273 -0
  137. package/src/core/backend-url.mjs +57 -0
  138. package/src/core/cache.mjs +105 -0
  139. package/src/core/callback-client.mjs +149 -0
  140. package/src/core/checkpoints.mjs +142 -0
  141. package/src/core/context-manager.mjs +198 -0
  142. package/src/core/headless.mjs +168 -0
  143. package/src/core/hooks-manager.mjs +87 -0
  144. package/src/core/jsonl-writer.mjs +351 -0
  145. package/src/core/local-agent.mjs +429 -0
  146. package/src/core/local-store.mjs +325 -0
  147. package/src/core/mode-selector.mjs +51 -0
  148. package/src/core/output-filter.mjs +177 -0
  149. package/src/core/paths.mjs +98 -0
  150. package/src/core/pricing.mjs +314 -0
  151. package/src/core/providers.mjs +219 -0
  152. package/src/core/rate-limiter.mjs +119 -0
  153. package/src/core/safety.mjs +200 -0
  154. package/src/core/scheduler.mjs +173 -0
  155. package/src/core/session-manager.mjs +317 -0
  156. package/src/core/session.mjs +143 -0
  157. package/src/core/settings-sync.mjs +85 -0
  158. package/src/core/stagnation.mjs +57 -0
  159. package/src/core/stream-client.mjs +367 -0
  160. package/src/core/streaming.mjs +182 -0
  161. package/src/core/system-prompt.mjs +135 -0
  162. package/src/core/tool-executor.mjs +725 -0
  163. package/src/hooks/engine.mjs +162 -0
  164. package/src/index.mjs +370 -0
  165. package/src/mcp/client.mjs +253 -0
  166. package/src/mcp/transport-shttp.mjs +130 -0
  167. package/src/mcp/transport-sse.mjs +131 -0
  168. package/src/mcp/transport-ws.mjs +134 -0
  169. package/src/permissions/checker.mjs +57 -0
  170. package/src/permissions/command-classifier.mjs +573 -0
  171. package/src/permissions/injection-check.mjs +60 -0
  172. package/src/permissions/path-check.mjs +102 -0
  173. package/src/permissions/prompt.mjs +73 -0
  174. package/src/permissions/sandbox.mjs +112 -0
  175. package/src/plugins/loader.mjs +138 -0
  176. package/src/skills/loader.mjs +147 -0
  177. package/src/skills/runner.mjs +55 -0
  178. package/src/telemetry/index.mjs +96 -0
  179. package/src/terminal/agents.mjs +177 -0
  180. package/src/terminal/analytics.mjs +292 -0
  181. package/src/terminal/ansi.mjs +421 -0
  182. package/src/terminal/main.mjs +150 -0
  183. package/src/terminal/repl.mjs +1484 -0
  184. package/src/terminal/tool-display.mjs +58 -0
  185. package/src/tools/agent.mjs +137 -0
  186. package/src/tools/ask-user.mjs +61 -0
  187. package/src/tools/bash.mjs +148 -0
  188. package/src/tools/cron-create.mjs +120 -0
  189. package/src/tools/cron-delete.mjs +49 -0
  190. package/src/tools/cron-list.mjs +37 -0
  191. package/src/tools/edit.mjs +82 -0
  192. package/src/tools/enter-worktree.mjs +69 -0
  193. package/src/tools/exit-worktree.mjs +57 -0
  194. package/src/tools/glob.mjs +117 -0
  195. package/src/tools/grep.mjs +129 -0
  196. package/src/tools/lint.mjs +71 -0
  197. package/src/tools/ls.mjs +58 -0
  198. package/src/tools/lsp.mjs +115 -0
  199. package/src/tools/multi-edit.mjs +94 -0
  200. package/src/tools/notebook-edit.mjs +96 -0
  201. package/src/tools/read-mcp-resource.mjs +57 -0
  202. package/src/tools/read.mjs +138 -0
  203. package/src/tools/registry.mjs +132 -0
  204. package/src/tools/remote-trigger.mjs +84 -0
  205. package/src/tools/send-message.mjs +64 -0
  206. package/src/tools/skill.mjs +52 -0
  207. package/src/tools/test-runner.mjs +49 -0
  208. package/src/tools/todo-write.mjs +68 -0
  209. package/src/tools/tool-search.mjs +77 -0
  210. package/src/tools/web-fetch.mjs +65 -0
  211. package/src/tools/web-search.mjs +89 -0
  212. package/src/tools/write.mjs +55 -0
  213. package/src/ui/banner.mjs +237 -0
  214. package/src/ui/commands.mjs +499 -0
  215. package/src/ui/formatter.mjs +379 -0
  216. package/src/ui/markdown.mjs +278 -0
  217. package/src/ui/slash-commands.mjs +258 -0
  218. package/index.js +0 -1
@@ -0,0 +1,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]">&quot;{value}&quot;</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">&quot;</span>
52
+ <span className="text-blue-700 dark:text-[#93c5fd]">{k}</span>
53
+ <span className="text-muted-foreground">&quot;</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
+ }