@bahulam/code 0.1.5 → 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 (179) 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 +112 -16
  21. package/src/core/tool-executor.mjs +123 -31
  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 +255 -55
  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/project-overview.mjs +5 -2
  51. package/src/tools/registry.mjs +4 -0
  52. package/src/ui/icons.mjs +2 -0
  53. package/src/ui/palette.mjs +1 -1
  54. package/src/ui/slash-commands.mjs +4 -6
  55. package/src/ui/tool-card.mjs +7 -3
  56. package/pulse/app/activity/page.tsx +0 -190
  57. package/pulse/app/api/activity/route.ts +0 -138
  58. package/pulse/app/api/benchmark/route.ts +0 -113
  59. package/pulse/app/api/benchmarks/route.ts +0 -195
  60. package/pulse/app/api/costs/route.ts +0 -88
  61. package/pulse/app/api/export/route.ts +0 -77
  62. package/pulse/app/api/history/route.ts +0 -11
  63. package/pulse/app/api/import/route.ts +0 -31
  64. package/pulse/app/api/memory/route.ts +0 -50
  65. package/pulse/app/api/plans/route.ts +0 -9
  66. package/pulse/app/api/projects/[slug]/route.ts +0 -96
  67. package/pulse/app/api/projects/route.ts +0 -121
  68. package/pulse/app/api/sessions/[id]/replay/route.ts +0 -20
  69. package/pulse/app/api/sessions/[id]/route.ts +0 -31
  70. package/pulse/app/api/sessions/route.ts +0 -112
  71. package/pulse/app/api/settings/route.ts +0 -14
  72. package/pulse/app/api/stats/route.ts +0 -143
  73. package/pulse/app/api/todos/route.ts +0 -9
  74. package/pulse/app/api/tools/route.ts +0 -160
  75. package/pulse/app/benchmarks/page.tsx +0 -224
  76. package/pulse/app/costs/page.tsx +0 -179
  77. package/pulse/app/export/page.tsx +0 -465
  78. package/pulse/app/favicon.ico +0 -0
  79. package/pulse/app/globals.css +0 -263
  80. package/pulse/app/help/page.tsx +0 -143
  81. package/pulse/app/history/page.tsx +0 -157
  82. package/pulse/app/layout.tsx +0 -46
  83. package/pulse/app/memory/page.tsx +0 -365
  84. package/pulse/app/overview-client.tsx +0 -393
  85. package/pulse/app/page.tsx +0 -14
  86. package/pulse/app/plans/page.tsx +0 -308
  87. package/pulse/app/projects/[slug]/page.tsx +0 -390
  88. package/pulse/app/projects/page.tsx +0 -110
  89. package/pulse/app/sessions/[id]/page.tsx +0 -243
  90. package/pulse/app/sessions/page.tsx +0 -39
  91. package/pulse/app/settings/page.tsx +0 -188
  92. package/pulse/app/todos/page.tsx +0 -211
  93. package/pulse/app/tools/page.tsx +0 -249
  94. package/pulse/cli.js +0 -164
  95. package/pulse/components/activity/day-of-week-chart.tsx +0 -35
  96. package/pulse/components/activity/streak-card.tsx +0 -36
  97. package/pulse/components/costs/cache-efficiency-panel.tsx +0 -76
  98. package/pulse/components/costs/cost-by-project-chart.tsx +0 -48
  99. package/pulse/components/costs/cost-over-time-chart.tsx +0 -95
  100. package/pulse/components/costs/model-token-table.tsx +0 -60
  101. package/pulse/components/global-search.tsx +0 -193
  102. package/pulse/components/keyboard-nav-provider.tsx +0 -23
  103. package/pulse/components/layout/bottom-nav.tsx +0 -53
  104. package/pulse/components/layout/client-layout.tsx +0 -31
  105. package/pulse/components/layout/sidebar-context.tsx +0 -50
  106. package/pulse/components/layout/sidebar.tsx +0 -183
  107. package/pulse/components/layout/top-bar.tsx +0 -121
  108. package/pulse/components/overview/activity-heatmap.tsx +0 -107
  109. package/pulse/components/overview/conversation-table.tsx +0 -148
  110. package/pulse/components/overview/model-breakdown-donut.tsx +0 -95
  111. package/pulse/components/overview/peak-hours-chart.tsx +0 -87
  112. package/pulse/components/overview/project-activity-donut.tsx +0 -96
  113. package/pulse/components/overview/stat-card.tsx +0 -102
  114. package/pulse/components/overview/usage-over-time-chart.tsx +0 -166
  115. package/pulse/components/projects/project-card.tsx +0 -175
  116. package/pulse/components/sessions/replay/assistant-markdown.tsx +0 -94
  117. package/pulse/components/sessions/replay/compaction-card.tsx +0 -25
  118. package/pulse/components/sessions/replay/session-sidebar.tsx +0 -231
  119. package/pulse/components/sessions/replay/token-accumulation-chart.tsx +0 -98
  120. package/pulse/components/sessions/replay/tool-call-badge.tsx +0 -127
  121. package/pulse/components/sessions/replay/turn-cards.tsx +0 -220
  122. package/pulse/components/sessions/replay/user-tool-result.tsx +0 -158
  123. package/pulse/components/sessions/session-badges.tsx +0 -49
  124. package/pulse/components/sessions/session-table.tsx +0 -299
  125. package/pulse/components/theme-provider.tsx +0 -44
  126. package/pulse/components/tools/feature-adoption-table.tsx +0 -58
  127. package/pulse/components/tools/mcp-server-panel.tsx +0 -45
  128. package/pulse/components/tools/tool-ranking-chart.tsx +0 -57
  129. package/pulse/components/tools/version-history-table.tsx +0 -32
  130. package/pulse/components/ui/alert.tsx +0 -66
  131. package/pulse/components/ui/badge.tsx +0 -48
  132. package/pulse/components/ui/breadcrumb.tsx +0 -109
  133. package/pulse/components/ui/button.tsx +0 -64
  134. package/pulse/components/ui/calendar.tsx +0 -220
  135. package/pulse/components/ui/card.tsx +0 -92
  136. package/pulse/components/ui/command.tsx +0 -158
  137. package/pulse/components/ui/dialog.tsx +0 -158
  138. package/pulse/components/ui/input.tsx +0 -21
  139. package/pulse/components/ui/popover.tsx +0 -89
  140. package/pulse/components/ui/progress.tsx +0 -31
  141. package/pulse/components/ui/select.tsx +0 -190
  142. package/pulse/components/ui/separator.tsx +0 -28
  143. package/pulse/components/ui/sheet.tsx +0 -143
  144. package/pulse/components/ui/skeleton.tsx +0 -13
  145. package/pulse/components/ui/table.tsx +0 -116
  146. package/pulse/components/ui/tabs.tsx +0 -91
  147. package/pulse/components/ui/tooltip.tsx +0 -57
  148. package/pulse/components/use-global-keyboard-nav.ts +0 -79
  149. package/pulse/components.json +0 -23
  150. package/pulse/eslint.config.mjs +0 -18
  151. package/pulse/lib/bahulam-paths.ts +0 -23
  152. package/pulse/lib/claude-reader.ts +0 -592
  153. package/pulse/lib/decode.ts +0 -129
  154. package/pulse/lib/pricing.ts +0 -102
  155. package/pulse/lib/replay-parser.ts +0 -165
  156. package/pulse/lib/tool-categories.ts +0 -140
  157. package/pulse/lib/utils.ts +0 -6
  158. package/pulse/next-env.d.ts +0 -6
  159. package/pulse/next.config.ts +0 -16
  160. package/pulse/package.json +0 -45
  161. package/pulse/postcss.config.mjs +0 -7
  162. package/pulse/public/activity.png +0 -0
  163. package/pulse/public/cc-lens.png +0 -0
  164. package/pulse/public/command-k.png +0 -0
  165. package/pulse/public/costs.png +0 -0
  166. package/pulse/public/dashboard-dark.png +0 -0
  167. package/pulse/public/dashboard-white.png +0 -0
  168. package/pulse/public/export.png +0 -0
  169. package/pulse/public/file.svg +0 -1
  170. package/pulse/public/globe.svg +0 -1
  171. package/pulse/public/next.svg +0 -1
  172. package/pulse/public/projects.png +0 -0
  173. package/pulse/public/session-chat.png +0 -0
  174. package/pulse/public/todos.png +0 -0
  175. package/pulse/public/tools.png +0 -0
  176. package/pulse/public/vercel.svg +0 -1
  177. package/pulse/public/window.svg +0 -1
  178. package/pulse/tsconfig.json +0 -34
  179. package/pulse/types/claude.ts +0 -294
@@ -1,179 +0,0 @@
1
- 'use client'
2
-
3
- import useSWR from 'swr'
4
- import { TopBar } from '@/components/layout/top-bar'
5
- import { CostOverTimeChart } from '@/components/costs/cost-over-time-chart'
6
- import { CostByProjectChart } from '@/components/costs/cost-by-project-chart'
7
- import { ModelTokenTable } from '@/components/costs/model-token-table'
8
- import { CacheEfficiencyPanel } from '@/components/costs/cache-efficiency-panel'
9
- import { formatCost, formatTokens } from '@/lib/decode'
10
- import { PRICING } from '@/lib/pricing'
11
- import type { CostAnalytics } 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 { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
16
- import { AlertTriangle, TrendingDown, DollarSign, Banknote } from 'lucide-react'
17
-
18
- const fetcher = (url: string) =>
19
- fetch(url).then(r => { if (!r.ok) throw new Error(`API error ${r.status}`); return r.json() })
20
-
21
- export default function CostsPage() {
22
- const { data, error, isLoading } = useSWR<CostAnalytics>('/api/costs', fetcher, { refreshInterval: 5_000 })
23
-
24
- return (
25
- <div className="flex flex-col min-h-screen">
26
- <TopBar title="Costs" subtitle="Estimated spend from ~/.bahulam/" />
27
- <div className="p-6 space-y-6">
28
-
29
- {error && (
30
- <Alert variant="destructive">
31
- <AlertTriangle className="h-4 w-4" />
32
- <AlertDescription>Error loading data: {String(error)}</AlertDescription>
33
- </Alert>
34
- )}
35
-
36
- {isLoading && (
37
- <div className="space-y-4">
38
- <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
39
- {Array.from({ length: 3 }).map((_, i) => <Skeleton key={i} className="h-28 rounded-xl" />)}
40
- </div>
41
- {Array.from({ length: 3 }).map((_, i) => <Skeleton key={i} className="h-56 rounded-xl" />)}
42
- </div>
43
- )}
44
-
45
- {data && (
46
- <>
47
- {/* Hero stat cards */}
48
- <div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
49
- <Card>
50
- <CardHeader className="pb-2">
51
- <CardDescription className="flex items-center gap-2">
52
- <DollarSign className="w-4 h-4" /> Total Estimated Cost
53
- </CardDescription>
54
- <CardTitle className="text-3xl font-bold tabular-nums text-[#d97706]">
55
- {formatCost(data.total_cost)}
56
- </CardTitle>
57
- </CardHeader>
58
- <CardContent>
59
- <p className="text-xs text-muted-foreground">All time spend across all projects</p>
60
- </CardContent>
61
- </Card>
62
-
63
- <Card>
64
- <CardHeader className="pb-2">
65
- <CardDescription className="flex items-center gap-2">
66
- <TrendingDown className="w-4 h-4" /> Cache Savings
67
- </CardDescription>
68
- <CardTitle className="text-3xl font-bold tabular-nums text-[#34d399]">
69
- {formatCost(data.total_savings)}
70
- </CardTitle>
71
- </CardHeader>
72
- <CardContent>
73
- <p className="text-xs text-muted-foreground">Saved by prompt caching</p>
74
- </CardContent>
75
- </Card>
76
-
77
- <Card>
78
- <CardHeader className="pb-2">
79
- <CardDescription className="flex items-center gap-2">
80
- <Banknote className="w-4 h-4" /> Without Cache
81
- </CardDescription>
82
- <CardTitle className="text-3xl font-bold tabular-nums text-red-400">
83
- {formatCost(data.total_cost + data.total_savings)}
84
- </CardTitle>
85
- </CardHeader>
86
- <CardContent>
87
- <p className="text-xs text-muted-foreground">What you would have spent</p>
88
- </CardContent>
89
- </Card>
90
- </div>
91
-
92
- {/* Cost over time */}
93
- {data.daily.length > 0 && (
94
- <Card>
95
- <CardHeader>
96
- <CardTitle>Cost Over Time</CardTitle>
97
- <CardDescription>Daily estimated spend</CardDescription>
98
- </CardHeader>
99
- <CardContent>
100
- <CostOverTimeChart daily={data.daily} />
101
- </CardContent>
102
- </Card>
103
- )}
104
-
105
- {/* Cost by project */}
106
- {data.by_project.length > 0 && (
107
- <Card>
108
- <CardHeader>
109
- <CardTitle>Cost by Project</CardTitle>
110
- <CardDescription>Spend breakdown across projects</CardDescription>
111
- </CardHeader>
112
- <CardContent>
113
- <CostByProjectChart projects={data.by_project} />
114
- </CardContent>
115
- </Card>
116
- )}
117
-
118
- {/* Per-model table */}
119
- <Card>
120
- <CardHeader>
121
- <CardTitle>Per-Model Token Breakdown</CardTitle>
122
- <CardDescription>Token usage and cost by model</CardDescription>
123
- </CardHeader>
124
- <CardContent>
125
- <ModelTokenTable models={data.models} />
126
- </CardContent>
127
- </Card>
128
-
129
- {/* Cache efficiency */}
130
- <Card>
131
- <CardHeader>
132
- <CardTitle>Cache Efficiency</CardTitle>
133
- <CardDescription>How much caching is saving you</CardDescription>
134
- </CardHeader>
135
- <CardContent>
136
- <CacheEfficiencyPanel models={data.models} totalSavings={data.total_savings} />
137
- </CardContent>
138
- </Card>
139
-
140
- {/* Pricing reference */}
141
- <Card>
142
- <CardHeader>
143
- <CardTitle>Pricing Reference</CardTitle>
144
- <CardDescription>
145
- Estimates only — update rates in{' '}
146
- <code className="text-xs bg-muted px-1 rounded">lib/pricing.ts</code>
147
- </CardDescription>
148
- </CardHeader>
149
- <CardContent>
150
- <Table>
151
- <TableHeader>
152
- <TableRow>
153
- <TableHead>Model</TableHead>
154
- <TableHead className="text-right">Input /MTok</TableHead>
155
- <TableHead className="text-right">Output /MTok</TableHead>
156
- <TableHead className="text-right">Cache Write /MTok</TableHead>
157
- <TableHead className="text-right">Cache Read /MTok</TableHead>
158
- </TableRow>
159
- </TableHeader>
160
- <TableBody>
161
- {Object.entries(PRICING).map(([model, p]) => (
162
- <TableRow key={model}>
163
- <TableCell className="font-mono text-sm">{model}</TableCell>
164
- <TableCell className="text-right font-mono text-blue-700 dark:text-[#60a5fa]">${(p.input * 1_000_000).toFixed(2)}</TableCell>
165
- <TableCell className="text-right font-mono text-[#d97706]">${(p.output * 1_000_000).toFixed(2)}</TableCell>
166
- <TableCell className="text-right font-mono text-[#a78bfa]">${(p.cacheWrite * 1_000_000).toFixed(2)}</TableCell>
167
- <TableCell className="text-right font-mono text-[#34d399]">${(p.cacheRead * 1_000_000).toFixed(2)}</TableCell>
168
- </TableRow>
169
- ))}
170
- </TableBody>
171
- </Table>
172
- </CardContent>
173
- </Card>
174
- </>
175
- )}
176
- </div>
177
- </div>
178
- )
179
- }
@@ -1,465 +0,0 @@
1
- 'use client'
2
-
3
- import { useState, useRef, useCallback, useMemo } from 'react'
4
- import useSWR from 'swr'
5
- import { TopBar } from '@/components/layout/top-bar'
6
- import type { ImportDiff } from '@/types/claude'
7
- import {
8
- Card,
9
- CardContent,
10
- CardDescription,
11
- CardHeader,
12
- CardTitle,
13
- } from '@/components/ui/card'
14
- import { Button } from '@/components/ui/button'
15
- import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
16
- import { Calendar } from '@/components/ui/calendar'
17
- import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
18
- import { Skeleton } from '@/components/ui/skeleton'
19
- import { Separator } from '@/components/ui/separator'
20
- import {
21
- Download,
22
- Upload,
23
- Database,
24
- Layers,
25
- History,
26
- FileJson2,
27
- AlertTriangle,
28
- CheckCircle2,
29
- Loader2,
30
- CalendarDays,
31
- } from 'lucide-react'
32
- import { format } from 'date-fns'
33
-
34
- const fetcher = (url: string) =>
35
- fetch(url).then(r => {
36
- if (!r.ok) throw new Error(`API error ${r.status}`)
37
- return r.json()
38
- })
39
-
40
- interface ExportPreview {
41
- sessionCount: number
42
- facetCount: number
43
- historyEntries: number
44
- hasStatsCache: boolean
45
- totalSessionsIndexed: number
46
- }
47
-
48
- function previewUrl(dateFrom: string, dateTo: string) {
49
- const p = new URLSearchParams()
50
- if (dateFrom) p.set('from', dateFrom)
51
- if (dateTo) p.set('to', dateTo)
52
- const q = p.toString()
53
- return `/api/export${q ? `?${q}` : ''}`
54
- }
55
-
56
- export default function ExportPage() {
57
- const [exporting, setExporting] = useState(false)
58
- const [exportRange, setExportRange] = useState<{ from?: Date; to?: Date }>({})
59
- const [exportPickerOpen, setExportPickerOpen] = useState(false)
60
- const [importDiff, setImportDiff] = useState<ImportDiff | null>(null)
61
- const [importError, setImportError] = useState('')
62
- const [importLoading, setImportLoading] = useState(false)
63
- const [dragging, setDragging] = useState(false)
64
- const fileRef = useRef<HTMLInputElement>(null)
65
-
66
- const rangeFromStr = exportRange.from ? format(exportRange.from, 'yyyy-MM-dd') : ''
67
- const rangeToStr = exportRange.to ? format(exportRange.to, 'yyyy-MM-dd') : ''
68
- const swrKey = useMemo(() => previewUrl(rangeFromStr, rangeToStr), [rangeFromStr, rangeToStr])
69
-
70
- const exportPickerLabel =
71
- exportRange.from && exportRange.to
72
- ? `${format(exportRange.from, 'MMM d')} – ${format(exportRange.to, 'MMM d, yyyy')}`
73
- : exportRange.from
74
- ? `${format(exportRange.from, 'MMM d, yyyy')} – …`
75
- : 'Pick date range (optional)'
76
- const { data: preview, error: previewError, isLoading: previewLoading } = useSWR<ExportPreview>(
77
- swrKey,
78
- fetcher,
79
- { refreshInterval: 30_000, keepPreviousData: true }
80
- )
81
-
82
- const dateFilterActive = Boolean(exportRange.from || exportRange.to)
83
-
84
- async function handleExport() {
85
- setExporting(true)
86
- try {
87
- const body: Record<string, unknown> = {}
88
- if (rangeFromStr || rangeToStr) {
89
- body.dateRange = { from: rangeFromStr || undefined, to: rangeToStr || undefined }
90
- }
91
-
92
- const res = await fetch('/api/export', {
93
- method: 'POST',
94
- headers: { 'Content-Type': 'application/json' },
95
- body: JSON.stringify(body),
96
- })
97
-
98
- const data = await res.json()
99
- const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
100
- const url = URL.createObjectURL(blob)
101
- const a = document.createElement('a')
102
- const date = new Date().toISOString().slice(0, 10)
103
- a.href = url
104
- a.download = `ccboard-export-${date}.ccboard.json`
105
- a.click()
106
- URL.revokeObjectURL(url)
107
- } finally {
108
- setExporting(false)
109
- }
110
- }
111
-
112
- async function processFile(file: File) {
113
- setImportError('')
114
- setImportDiff(null)
115
- setImportLoading(true)
116
- try {
117
- const text = await file.text()
118
- const json = JSON.parse(text)
119
- const res = await fetch('/api/import', {
120
- method: 'POST',
121
- headers: { 'Content-Type': 'application/json' },
122
- body: JSON.stringify(json),
123
- })
124
- if (!res.ok) {
125
- const err = await res.json()
126
- setImportError(err.error ?? 'Import failed')
127
- return
128
- }
129
- const diff = await res.json() as ImportDiff
130
- setImportDiff(diff)
131
- } catch (e) {
132
- setImportError(String(e))
133
- } finally {
134
- setImportLoading(false)
135
- }
136
- }
137
-
138
- const onDrop = useCallback((e: React.DragEvent) => {
139
- e.preventDefault()
140
- setDragging(false)
141
- const file = e.dataTransfer.files[0]
142
- if (file) processFile(file)
143
- }, [])
144
-
145
- return (
146
- <div className="flex flex-col min-h-screen">
147
- <TopBar
148
- title="Export & import"
149
- subtitle="Download a portable backup of ~/.bahulam/ analytics or merge data from another machine"
150
- />
151
-
152
- <div className="p-6 space-y-6 flex-1">
153
- {previewError && (
154
- <Alert variant="destructive">
155
- <AlertTriangle className="h-4 w-4" />
156
- <AlertTitle>Could not load export preview</AlertTitle>
157
- <AlertDescription>{String(previewError)}</AlertDescription>
158
- </Alert>
159
- )}
160
-
161
- {/* Summary — fills the page so it never feels empty */}
162
- <section className="space-y-3">
163
- <div>
164
- <h2 className="text-lg font-semibold tracking-tight">What will be exported</h2>
165
- <p className="text-sm text-muted-foreground mt-1">
166
- One JSON file includes stats cache, session metadata, facets, and command history. Numbers below respect the optional date range.
167
- </p>
168
- </div>
169
- <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
170
- {previewLoading && !preview ? (
171
- <>
172
- {Array.from({ length: 4 }).map((_, i) => (
173
- <Skeleton key={i} className="h-[104px] rounded-xl" />
174
- ))}
175
- </>
176
- ) : (
177
- <>
178
- <Card className="shadow-sm">
179
- <CardHeader className="pb-2">
180
- <CardDescription className="flex items-center gap-2">
181
- <Database className="size-4 text-primary" />
182
- Sessions
183
- </CardDescription>
184
- <CardTitle className="text-2xl tabular-nums">
185
- {preview?.sessionCount ?? '—'}
186
- </CardTitle>
187
- </CardHeader>
188
- <CardContent className="pt-0">
189
- <p className="text-xs text-muted-foreground">
190
- {dateFilterActive
191
- ? 'In selected range'
192
- : `of ${preview?.totalSessionsIndexed ?? '—'} indexed`}
193
- </p>
194
- </CardContent>
195
- </Card>
196
- <Card className="shadow-sm">
197
- <CardHeader className="pb-2">
198
- <CardDescription className="flex items-center gap-2">
199
- <Layers className="size-4 text-primary" />
200
- Facets
201
- </CardDescription>
202
- <CardTitle className="text-2xl tabular-nums">
203
- {preview?.facetCount ?? '—'}
204
- </CardTitle>
205
- </CardHeader>
206
- <CardContent className="pt-0">
207
- <p className="text-xs text-muted-foreground">Linked to exported sessions</p>
208
- </CardContent>
209
- </Card>
210
- <Card className="shadow-sm">
211
- <CardHeader className="pb-2">
212
- <CardDescription className="flex items-center gap-2">
213
- <History className="size-4 text-primary" />
214
- History rows
215
- </CardDescription>
216
- <CardTitle className="text-2xl tabular-nums">
217
- {preview?.historyEntries ?? '—'}
218
- </CardTitle>
219
- </CardHeader>
220
- <CardContent className="pt-0">
221
- <p className="text-xs text-muted-foreground">Recent entries (up to 10k)</p>
222
- </CardContent>
223
- </Card>
224
- <Card className="shadow-sm">
225
- <CardHeader className="pb-2">
226
- <CardDescription className="flex items-center gap-2">
227
- <FileJson2 className="size-4 text-primary" />
228
- Stats cache
229
- </CardDescription>
230
- <CardTitle className="text-base font-medium">
231
- {preview?.hasStatsCache ? (
232
- <span className="text-emerald-600 dark:text-emerald-400 flex items-center gap-1.5">
233
- <CheckCircle2 className="size-5 shrink-0" />
234
- Included
235
- </span>
236
- ) : (
237
- <span className="text-muted-foreground">Not found</span>
238
- )}
239
- </CardTitle>
240
- </CardHeader>
241
- <CardContent className="pt-0">
242
- <p className="text-xs text-muted-foreground">From ~/.bahulam/ when available</p>
243
- </CardContent>
244
- </Card>
245
- </>
246
- )}
247
- </div>
248
- </section>
249
-
250
- <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 lg:items-stretch">
251
- {/* Export */}
252
- <Card className="shadow-sm border-border/80 flex h-full min-h-0 flex-col">
253
- <CardHeader>
254
- <div className="flex items-start gap-3">
255
- <div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
256
- <Download className="size-5" />
257
- </div>
258
- <div className="space-y-1">
259
- <CardTitle>Export</CardTitle>
260
- <CardDescription>
261
- Download <code className="rounded bg-muted px-1 py-0.5 text-xs">.ccboard.json</code> for backup or
262
- another machine.
263
- </CardDescription>
264
- </div>
265
- </div>
266
- </CardHeader>
267
- <CardContent className="flex flex-1 flex-col gap-5 min-h-0">
268
- <div className="space-y-2">
269
- <span className="text-sm font-medium leading-none">Date range (optional)</span>
270
- <div className="flex flex-wrap items-center gap-2">
271
- <Popover open={exportPickerOpen} onOpenChange={setExportPickerOpen}>
272
- <PopoverTrigger asChild>
273
- <Button
274
- type="button"
275
- variant={dateFilterActive ? 'default' : 'outline'}
276
- size="sm"
277
- className="gap-2 justify-start"
278
- >
279
- <CalendarDays className="w-3.5 h-3.5 shrink-0" />
280
- <span className="truncate">{exportPickerLabel}</span>
281
- </Button>
282
- </PopoverTrigger>
283
- <PopoverContent className="w-auto p-0" align="start">
284
- <Calendar
285
- mode="range"
286
- selected={{ from: exportRange.from, to: exportRange.to }}
287
- onSelect={range => {
288
- setExportRange({ from: range?.from, to: range?.to })
289
- if (range?.from && range?.to) setExportPickerOpen(false)
290
- }}
291
- disabled={{ after: new Date() }}
292
- initialFocus
293
- />
294
- </PopoverContent>
295
- </Popover>
296
- {dateFilterActive && (
297
- <Button
298
- type="button"
299
- variant="ghost"
300
- size="sm"
301
- className="h-8 text-muted-foreground"
302
- onClick={() => setExportRange({})}
303
- >
304
- Clear
305
- </Button>
306
- )}
307
- </div>
308
- </div>
309
- {dateFilterActive && (
310
- <p className="text-xs text-muted-foreground rounded-md bg-muted/50 px-3 py-2 border border-border/60">
311
- Only sessions whose <span className="font-medium text-foreground/80">start time</span> falls in this
312
- range are included. Use <span className="font-medium text-foreground/80">Clear</span> for a full export.
313
- </p>
314
- )}
315
- <div className="mt-auto flex flex-col gap-5">
316
- <Separator />
317
- <Button
318
- className="w-full sm:w-auto"
319
- size="lg"
320
- onClick={handleExport}
321
- disabled={exporting || previewLoading}
322
- >
323
- {exporting ? (
324
- <>
325
- <Loader2 className="size-4 animate-spin" />
326
- Preparing download…
327
- </>
328
- ) : (
329
- <>
330
- <Download className="size-4" />
331
- Download export
332
- </>
333
- )}
334
- </Button>
335
- </div>
336
- </CardContent>
337
- </Card>
338
-
339
- {/* Import */}
340
- <Card className="shadow-sm border-border/80 flex h-full min-h-0 flex-col">
341
- <CardHeader>
342
- <div className="flex items-start gap-3">
343
- <div className="flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted text-foreground">
344
- <Upload className="size-5" />
345
- </div>
346
- <div className="space-y-1">
347
- <CardTitle>Import / merge</CardTitle>
348
- <CardDescription>
349
- Drop a file from another machine. Merge is <strong className="text-foreground/90">additive only</strong>
350
- — existing sessions are never overwritten.
351
- </CardDescription>
352
- </div>
353
- </div>
354
- </CardHeader>
355
- <CardContent className="flex flex-1 flex-col gap-4 min-h-0">
356
- <button
357
- type="button"
358
- onDragOver={e => {
359
- e.preventDefault()
360
- setDragging(true)
361
- }}
362
- onDragLeave={() => setDragging(false)}
363
- onDrop={onDrop}
364
- onClick={() => fileRef.current?.click()}
365
- className={`
366
- flex w-full flex-1 min-h-[220px] flex-col items-center justify-center rounded-xl border-2 border-dashed px-4 py-8 text-center transition-colors
367
- ${dragging ? 'border-primary bg-primary/10' : 'border-border hover:border-primary/40 hover:bg-muted/40'}
368
- `}
369
- >
370
- <Upload className="size-8 mb-3 text-muted-foreground opacity-80" />
371
- <p className="text-sm text-foreground font-medium">Drop .ccboard.json here</p>
372
- <p className="text-xs text-muted-foreground mt-1">
373
- or <span className="text-primary underline underline-offset-2">browse files</span>
374
- </p>
375
- <input
376
- ref={fileRef}
377
- type="file"
378
- accept=".json,.ccboard.json,application/json"
379
- className="hidden"
380
- onChange={e => {
381
- const file = e.target.files?.[0]
382
- if (file) processFile(file)
383
- }}
384
- />
385
- </button>
386
-
387
- {importLoading && (
388
- <div className="flex items-center gap-2 text-sm text-muted-foreground">
389
- <Loader2 className="size-4 animate-spin" />
390
- Analyzing file…
391
- </div>
392
- )}
393
-
394
- {importError && (
395
- <Alert variant="destructive">
396
- <AlertTriangle className="h-4 w-4" />
397
- <AlertDescription className="font-mono text-xs">{importError}</AlertDescription>
398
- </Alert>
399
- )}
400
-
401
- {importDiff && (
402
- <div className="rounded-lg border bg-muted/30 p-4 space-y-3 text-sm">
403
- <p className="font-semibold text-foreground flex items-center gap-2">
404
- <FileJson2 className="size-4 opacity-70" />
405
- Merge preview
406
- </p>
407
- <dl className="grid gap-2 text-muted-foreground">
408
- <div className="flex justify-between gap-4">
409
- <dt>Sessions in file</dt>
410
- <dd className="font-mono text-foreground font-semibold tabular-nums">
411
- {importDiff.total_in_export}
412
- </dd>
413
- </div>
414
- <div className="flex justify-between gap-4">
415
- <dt>Already present (skipped)</dt>
416
- <dd className="font-mono tabular-nums text-muted-foreground/80">{importDiff.already_present}</dd>
417
- </div>
418
- <div className="flex justify-between gap-4">
419
- <dt>New sessions to add</dt>
420
- <dd
421
- className={`font-mono font-semibold tabular-nums ${
422
- importDiff.new_sessions > 0 ? 'text-emerald-600 dark:text-emerald-400' : 'text-muted-foreground/60'
423
- }`}
424
- >
425
- {importDiff.new_sessions}
426
- </dd>
427
- </div>
428
- </dl>
429
-
430
- {importDiff.new_sessions === 0 ? (
431
- <p className="text-xs text-muted-foreground pt-1 border-t border-border/60">
432
- Everything in this file is already in your index. Nothing to merge.
433
- </p>
434
- ) : (
435
- <div className="space-y-2 pt-1 border-t border-border/60">
436
- <Alert className="border-amber-500/40 bg-amber-500/5">
437
- <AlertTriangle className="h-4 w-4 text-amber-600" />
438
- <AlertDescription className="text-xs text-amber-800 dark:text-amber-200/90">
439
- Writing merged data to ~/.bahulam/ is not implemented in this build — this is a preview only.
440
- </AlertDescription>
441
- </Alert>
442
- <div className="max-h-36 overflow-auto rounded-md border border-border/60 bg-background/50 space-y-1 p-2 font-mono text-[11px] text-muted-foreground">
443
- {importDiff.sessions_to_add.slice(0, 12).map(s => (
444
- <div key={s.session_id}>
445
- + {s.session_id.slice(0, 8)}… · {s.start_time.slice(0, 10)} ·{' '}
446
- {s.project_path?.split('/').slice(-1)[0] ?? '—'}
447
- </div>
448
- ))}
449
- {importDiff.sessions_to_add.length > 12 && (
450
- <p className="text-muted-foreground/50 pt-1">
451
- …and {importDiff.sessions_to_add.length - 12} more
452
- </p>
453
- )}
454
- </div>
455
- </div>
456
- )}
457
- </div>
458
- )}
459
- </CardContent>
460
- </Card>
461
- </div>
462
- </div>
463
- </div>
464
- )
465
- }
Binary file