@bahulam/code 0.1.6 → 0.1.7

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 (178) hide show
  1. package/LICENSE +1 -1
  2. package/NOTICE +6 -6
  3. package/README.md +10 -17
  4. package/package.json +9 -9
  5. package/src/agents/loader.mjs +1 -1
  6. package/src/agents/workflow_loader.mjs +2 -2
  7. package/src/auth/tarang-auth.mjs +3 -2
  8. package/src/config/model-catalog-default.json +4 -3
  9. package/src/config/model-catalog.mjs +2 -0
  10. package/src/context/prose-chunker.mjs +162 -5
  11. package/src/core/attachments.mjs +65 -3
  12. package/src/core/bundled-runtime.mjs +1 -1
  13. package/src/core/headless.mjs +14 -0
  14. package/src/core/jsonl-writer.mjs +3 -3
  15. package/src/core/lint-resolver.mjs +472 -0
  16. package/src/core/local-agent.mjs +15 -4
  17. package/src/core/local-store.mjs +4 -0
  18. package/src/core/output-filter.mjs +21 -25
  19. package/src/core/pricing.mjs +10 -3
  20. package/src/core/stream-client.mjs +9 -4
  21. package/src/core/tool-executor.mjs +105 -29
  22. package/src/daemon/http-dashboard.mjs +400 -0
  23. package/src/daemon/session-core.mjs +1 -0
  24. package/src/local-service/agent-relay.mjs +868 -0
  25. package/src/local-service/approval-bridge.mjs +222 -0
  26. package/src/local-service/browser.mjs +24 -0
  27. package/src/local-service/command.mjs +155 -0
  28. package/src/local-service/file-access.mjs +393 -0
  29. package/src/local-service/machine.mjs +86 -0
  30. package/src/local-service/paths.mjs +29 -0
  31. package/src/local-service/preview-converters.mjs +260 -0
  32. package/src/local-service/server.mjs +2644 -0
  33. package/src/local-service/session-store.mjs +221 -0
  34. package/src/onboarding/preflight.mjs +1 -1
  35. package/src/terminal/analytics.mjs +2 -6
  36. package/src/terminal/ansi.mjs +11 -3
  37. package/src/terminal/main.mjs +37 -11
  38. package/src/terminal/model-catalog-display.mjs +111 -0
  39. package/src/terminal/repl-format.mjs +4 -1
  40. package/src/terminal/repl-model-form.mjs +81 -14
  41. package/src/terminal/repl-render.mjs +35 -5
  42. package/src/terminal/repl-state.mjs +12 -0
  43. package/src/terminal/repl.mjs +231 -53
  44. package/src/terminal/tool-display.mjs +32 -0
  45. package/src/terminal/watch-state.mjs +118 -0
  46. package/src/tools/analyze-image.mjs +158 -0
  47. package/src/tools/bash.mjs +11 -9
  48. package/src/tools/generate-image.mjs +411 -0
  49. package/src/tools/lint.mjs +24 -43
  50. package/src/tools/registry.mjs +4 -0
  51. package/src/ui/icons.mjs +2 -0
  52. package/src/ui/palette.mjs +1 -1
  53. package/src/ui/slash-commands.mjs +4 -6
  54. package/src/ui/tool-card.mjs +7 -3
  55. package/pulse/app/activity/page.tsx +0 -190
  56. package/pulse/app/api/activity/route.ts +0 -138
  57. package/pulse/app/api/benchmark/route.ts +0 -113
  58. package/pulse/app/api/benchmarks/route.ts +0 -195
  59. package/pulse/app/api/costs/route.ts +0 -88
  60. package/pulse/app/api/export/route.ts +0 -77
  61. package/pulse/app/api/history/route.ts +0 -11
  62. package/pulse/app/api/import/route.ts +0 -31
  63. package/pulse/app/api/memory/route.ts +0 -50
  64. package/pulse/app/api/plans/route.ts +0 -9
  65. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  66. package/pulse/app/api/projects/route.ts +0 -121
  67. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  68. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  69. package/pulse/app/api/sessions/route.ts +0 -112
  70. package/pulse/app/api/settings/route.ts +0 -14
  71. package/pulse/app/api/stats/route.ts +0 -143
  72. package/pulse/app/api/todos/route.ts +0 -9
  73. package/pulse/app/api/tools/route.ts +0 -160
  74. package/pulse/app/benchmarks/page.tsx +0 -224
  75. package/pulse/app/costs/page.tsx +0 -179
  76. package/pulse/app/export/page.tsx +0 -465
  77. package/pulse/app/favicon.ico +0 -0
  78. package/pulse/app/globals.css +0 -263
  79. package/pulse/app/help/page.tsx +0 -143
  80. package/pulse/app/history/page.tsx +0 -157
  81. package/pulse/app/layout.tsx +0 -46
  82. package/pulse/app/memory/page.tsx +0 -365
  83. package/pulse/app/overview-client.tsx +0 -393
  84. package/pulse/app/page.tsx +0 -14
  85. package/pulse/app/plans/page.tsx +0 -308
  86. package/pulse/app/projects/[slug]/page.tsx +0 -390
  87. package/pulse/app/projects/page.tsx +0 -110
  88. package/pulse/app/sessions/[id]/page.tsx +0 -243
  89. package/pulse/app/sessions/page.tsx +0 -39
  90. package/pulse/app/settings/page.tsx +0 -188
  91. package/pulse/app/todos/page.tsx +0 -211
  92. package/pulse/app/tools/page.tsx +0 -249
  93. package/pulse/cli.js +0 -164
  94. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  95. package/pulse/components/activity/streak-card.tsx +0 -36
  96. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  97. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  98. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  99. package/pulse/components/costs/model-token-table.tsx +0 -60
  100. package/pulse/components/global-search.tsx +0 -193
  101. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  102. package/pulse/components/layout/bottom-nav.tsx +0 -53
  103. package/pulse/components/layout/client-layout.tsx +0 -31
  104. package/pulse/components/layout/sidebar-context.tsx +0 -50
  105. package/pulse/components/layout/sidebar.tsx +0 -183
  106. package/pulse/components/layout/top-bar.tsx +0 -121
  107. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  108. package/pulse/components/overview/conversation-table.tsx +0 -148
  109. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  110. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  111. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  112. package/pulse/components/overview/stat-card.tsx +0 -102
  113. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  114. package/pulse/components/projects/project-card.tsx +0 -175
  115. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  116. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  117. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  118. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  119. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  120. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  121. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  122. package/pulse/components/sessions/session-badges.tsx +0 -49
  123. package/pulse/components/sessions/session-table.tsx +0 -299
  124. package/pulse/components/theme-provider.tsx +0 -44
  125. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  126. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  127. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  128. package/pulse/components/tools/version-history-table.tsx +0 -32
  129. package/pulse/components/ui/alert.tsx +0 -66
  130. package/pulse/components/ui/badge.tsx +0 -48
  131. package/pulse/components/ui/breadcrumb.tsx +0 -109
  132. package/pulse/components/ui/button.tsx +0 -64
  133. package/pulse/components/ui/calendar.tsx +0 -220
  134. package/pulse/components/ui/card.tsx +0 -92
  135. package/pulse/components/ui/command.tsx +0 -158
  136. package/pulse/components/ui/dialog.tsx +0 -158
  137. package/pulse/components/ui/input.tsx +0 -21
  138. package/pulse/components/ui/popover.tsx +0 -89
  139. package/pulse/components/ui/progress.tsx +0 -31
  140. package/pulse/components/ui/select.tsx +0 -190
  141. package/pulse/components/ui/separator.tsx +0 -28
  142. package/pulse/components/ui/sheet.tsx +0 -143
  143. package/pulse/components/ui/skeleton.tsx +0 -13
  144. package/pulse/components/ui/table.tsx +0 -116
  145. package/pulse/components/ui/tabs.tsx +0 -91
  146. package/pulse/components/ui/tooltip.tsx +0 -57
  147. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  148. package/pulse/components.json +0 -23
  149. package/pulse/eslint.config.mjs +0 -18
  150. package/pulse/lib/bahulam-paths.ts +0 -23
  151. package/pulse/lib/claude-reader.ts +0 -592
  152. package/pulse/lib/decode.ts +0 -129
  153. package/pulse/lib/pricing.ts +0 -102
  154. package/pulse/lib/replay-parser.ts +0 -165
  155. package/pulse/lib/tool-categories.ts +0 -140
  156. package/pulse/lib/utils.ts +0 -6
  157. package/pulse/next-env.d.ts +0 -6
  158. package/pulse/next.config.ts +0 -16
  159. package/pulse/package.json +0 -45
  160. package/pulse/postcss.config.mjs +0 -7
  161. package/pulse/public/activity.png +0 -0
  162. package/pulse/public/cc-lens.png +0 -0
  163. package/pulse/public/command-k.png +0 -0
  164. package/pulse/public/costs.png +0 -0
  165. package/pulse/public/dashboard-dark.png +0 -0
  166. package/pulse/public/dashboard-white.png +0 -0
  167. package/pulse/public/export.png +0 -0
  168. package/pulse/public/file.svg +0 -1
  169. package/pulse/public/globe.svg +0 -1
  170. package/pulse/public/next.svg +0 -1
  171. package/pulse/public/projects.png +0 -0
  172. package/pulse/public/session-chat.png +0 -0
  173. package/pulse/public/todos.png +0 -0
  174. package/pulse/public/tools.png +0 -0
  175. package/pulse/public/vercel.svg +0 -1
  176. package/pulse/public/window.svg +0 -1
  177. package/pulse/tsconfig.json +0 -34
  178. package/pulse/types/claude.ts +0 -294
@@ -1,243 +0,0 @@
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
- }
@@ -1,39 +0,0 @@
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="Bahulam 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
- }
@@ -1,188 +0,0 @@
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="Bahulam Pulse · settings" subtitle="~/.bahulam/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 ~/.bahulam/</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 ~/.bahulam/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 ~/.bahulam/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
- }
@@ -1,211 +0,0 @@
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="~/.bahulam/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 ~/.bahulam/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
- }