agentfit 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +30 -34
  2. package/app/(dashboard)/daily/page.tsx +1 -1
  3. package/app/(dashboard)/flow/page.tsx +17 -0
  4. package/app/(dashboard)/layout.tsx +2 -0
  5. package/app/(dashboard)/page.tsx +24 -5
  6. package/app/(dashboard)/reports/[id]/page.tsx +72 -0
  7. package/app/(dashboard)/reports/page.tsx +132 -0
  8. package/app/(dashboard)/sessions/[id]/page.tsx +167 -0
  9. package/app/(dashboard)/settings/page.tsx +180 -0
  10. package/app/api/backup/route.ts +215 -0
  11. package/app/api/check/route.ts +11 -1
  12. package/app/api/command-insights/route.ts +13 -0
  13. package/app/api/images-analysis/route.ts +3 -4
  14. package/app/api/reports/[id]/route.ts +23 -0
  15. package/app/api/reports/route.ts +50 -0
  16. package/app/api/reset/route.ts +21 -0
  17. package/app/api/session/route.ts +40 -0
  18. package/app/api/usage/route.ts +25 -1
  19. package/app/layout.tsx +1 -1
  20. package/bin/agentfit.mjs +2 -2
  21. package/components/agent-coach.tsx +256 -129
  22. package/components/app-sidebar.tsx +258 -8
  23. package/components/backup-section.tsx +236 -0
  24. package/components/daily-chart.tsx +404 -83
  25. package/components/dashboard-shell.tsx +9 -24
  26. package/components/data-provider.tsx +66 -2
  27. package/components/fitness-score.tsx +95 -54
  28. package/components/overview-cards.tsx +148 -41
  29. package/components/report-view.tsx +307 -0
  30. package/components/screenshots-analysis.tsx +51 -46
  31. package/components/session-chatlog.tsx +124 -0
  32. package/components/session-timeline.tsx +184 -0
  33. package/components/session-workflow.tsx +183 -0
  34. package/components/sessions-table.tsx +9 -1
  35. package/components/tool-flow-graph.tsx +144 -0
  36. package/components/ui/carousel.tsx +242 -0
  37. package/components/ui/sidebar.tsx +1 -1
  38. package/components/ui/sonner.tsx +51 -0
  39. package/generated/prisma/browser.ts +5 -0
  40. package/generated/prisma/client.ts +5 -0
  41. package/generated/prisma/internal/class.ts +14 -4
  42. package/generated/prisma/internal/prismaNamespace.ts +96 -2
  43. package/generated/prisma/internal/prismaNamespaceBrowser.ts +20 -1
  44. package/generated/prisma/models/Report.ts +1219 -0
  45. package/generated/prisma/models/Session.ts +187 -1
  46. package/generated/prisma/models.ts +1 -0
  47. package/lib/coach.ts +530 -211
  48. package/lib/command-insights.ts +231 -0
  49. package/lib/db.ts +1 -1
  50. package/lib/parse-codex.ts +5 -0
  51. package/lib/parse-logs.ts +65 -0
  52. package/lib/queries-codex.ts +22 -0
  53. package/lib/queries.ts +42 -0
  54. package/lib/report.ts +156 -0
  55. package/lib/session-detail.ts +382 -0
  56. package/lib/sync.ts +77 -0
  57. package/lib/tool-flow.ts +71 -0
  58. package/next.config.mjs +6 -1
  59. package/package.json +16 -2
  60. package/plugins/cost-heatmap/component.tsx +72 -50
  61. package/prisma/schema.prisma +17 -0
  62. package/.claude/settings.local.json +0 -26
  63. package/CONTRIBUTING.md +0 -209
  64. package/prisma/migrations/20260328152517_init/migration.sql +0 -41
  65. package/prisma/migrations/20260328153801_add_image_model/migration.sql +0 -18
  66. package/prisma/migrations/migration_lock.toml +0 -3
  67. package/prisma.config.ts +0 -14
  68. package/setup.sh +0 -73
@@ -1,20 +1,31 @@
1
1
  'use client'
2
2
 
3
- import { useState } from 'react'
3
+ import { useState, useEffect, useCallback, type ReactNode } from 'react'
4
4
  import Link from 'next/link'
5
5
  import { usePathname } from 'next/navigation'
6
6
  import {
7
+ AlertTriangle,
7
8
  BarChart3,
8
9
  Brain,
9
10
  Camera,
11
+ Check,
10
12
  ChevronRight,
13
+ Cloud,
14
+ CloudOff,
11
15
  Coins,
16
+ FileText,
12
17
  FolderOpen,
18
+ GitBranch,
13
19
  HeartPulse,
14
20
  LayoutDashboard,
15
21
  ListTree,
22
+ Loader2,
16
23
  Puzzle,
24
+ RefreshCw,
25
+ RotateCcw,
26
+ Settings,
17
27
  Terminal,
28
+ Upload,
18
29
  Wrench,
19
30
  } from 'lucide-react'
20
31
  import type { ComponentType } from 'react'
@@ -24,6 +35,7 @@ import {
24
35
  SidebarGroup,
25
36
  SidebarGroupContent,
26
37
  SidebarGroupLabel,
38
+ SidebarFooter,
27
39
  SidebarHeader,
28
40
  SidebarMenu,
29
41
  SidebarMenuButton,
@@ -33,11 +45,23 @@ import {
33
45
  SidebarMenuSubItem,
34
46
  SidebarRail,
35
47
  } from '@/components/ui/sidebar'
48
+ import {
49
+ Dialog,
50
+ DialogContent,
51
+ DialogDescription,
52
+ DialogFooter,
53
+ DialogHeader,
54
+ DialogTitle,
55
+ } from '@/components/ui/dialog'
56
+ import { Button } from '@/components/ui/button'
57
+ import { Badge } from '@/components/ui/badge'
58
+ import { useData } from './data-provider'
36
59
  import { getPlugins } from '@/lib/plugins'
60
+ import { toast } from 'sonner'
37
61
  import '@/plugins'
38
62
 
39
63
  interface NavItem {
40
- title: string
64
+ title: ReactNode
41
65
  icon: ComponentType<{ className?: string }>
42
66
  href: string
43
67
  }
@@ -50,11 +74,19 @@ interface NavGroup {
50
74
 
51
75
  const topItems: NavItem[] = [
52
76
  { title: 'Dashboard', icon: LayoutDashboard, href: '/' },
53
- { title: 'Agent Coach', icon: HeartPulse, href: '/coach' },
54
- { title: 'Projects', icon: FolderOpen, href: '/projects' },
77
+ { title: 'CRAFT Coach', icon: HeartPulse, href: '/coach' },
78
+ { title: 'Reports', icon: FileText, href: '/reports' },
55
79
  ]
56
80
 
57
81
  const navGroups: NavGroup[] = [
82
+ {
83
+ title: 'Projects',
84
+ icon: FolderOpen,
85
+ items: [
86
+ { title: 'Overview', icon: FolderOpen, href: '/projects' },
87
+ { title: 'Sessions', icon: ListTree, href: '/sessions' },
88
+ ],
89
+ },
58
90
  {
59
91
  title: 'Usage',
60
92
  icon: BarChart3,
@@ -70,6 +102,7 @@ const navGroups: NavGroup[] = [
70
102
  items: [
71
103
  { title: 'Personality Fit', icon: Brain, href: '/personality' },
72
104
  { title: 'Command Usage', icon: Terminal, href: '/commands' },
105
+ { title: 'Session Flow', icon: GitBranch, href: '/flow' },
73
106
  { title: 'Images', icon: Camera, href: '/images' },
74
107
  ],
75
108
  },
@@ -106,6 +139,194 @@ function CollapsibleGroup({ group, pathname }: { group: NavGroup; pathname: stri
106
139
  )
107
140
  }
108
141
 
142
+ interface BackupFolderStatus {
143
+ exists: boolean
144
+ hasGit: boolean
145
+ hasRemote: boolean
146
+ remoteUrl: string | null
147
+ isDirty: boolean
148
+ lastCommit: string | null
149
+ }
150
+
151
+ interface BackupStatus {
152
+ ghInstalled: boolean
153
+ ghAuthenticated: boolean
154
+ ghUser: string | null
155
+ claude: BackupFolderStatus
156
+ codex: BackupFolderStatus
157
+ }
158
+
159
+ function BackupSection() {
160
+ const [status, setStatus] = useState<BackupStatus | null>(null)
161
+ const [loading, setLoading] = useState(false)
162
+ const [syncing, setSyncing] = useState<Record<string, boolean>>({})
163
+
164
+ const fetchStatus = useCallback(async () => {
165
+ setLoading(true)
166
+ try {
167
+ const res = await fetch('/api/backup')
168
+ setStatus(await res.json())
169
+ } catch {
170
+ // ignore
171
+ } finally {
172
+ setLoading(false)
173
+ }
174
+ }, [])
175
+
176
+ useEffect(() => {
177
+ fetchStatus()
178
+ }, [fetchStatus])
179
+
180
+ const handleInit = async (folder: 'claude' | 'codex') => {
181
+ setSyncing((s) => ({ ...s, [folder]: true }))
182
+ try {
183
+ const res = await fetch('/api/backup', {
184
+ method: 'POST',
185
+ headers: { 'Content-Type': 'application/json' },
186
+ body: JSON.stringify({ action: 'init', folder }),
187
+ })
188
+ const data = await res.json()
189
+ if (!res.ok) {
190
+ toast.error(`Backup failed`, { description: data.error })
191
+ return
192
+ }
193
+ toast.success(`~/.${folder} backed up`, {
194
+ description: (
195
+ <a href={data.repoUrl} target="_blank" rel="noopener noreferrer" className="underline">
196
+ {data.repoUrl}
197
+ </a>
198
+ ),
199
+ })
200
+ await fetchStatus()
201
+ } catch (e) {
202
+ toast.error('Backup failed', { description: (e as Error).message })
203
+ } finally {
204
+ setSyncing((s) => ({ ...s, [folder]: false }))
205
+ }
206
+ }
207
+
208
+ const handleSync = async (folder: 'claude' | 'codex') => {
209
+ setSyncing((s) => ({ ...s, [folder]: true }))
210
+ try {
211
+ const res = await fetch('/api/backup', {
212
+ method: 'POST',
213
+ headers: { 'Content-Type': 'application/json' },
214
+ body: JSON.stringify({ action: 'sync', folder }),
215
+ })
216
+ const data = await res.json()
217
+ if (!res.ok) {
218
+ toast.error(`Sync failed`, { description: data.error })
219
+ return
220
+ }
221
+ toast.success(`~/.${folder} synced`, { description: data.message })
222
+ await fetchStatus()
223
+ } catch (e) {
224
+ toast.error('Sync failed', { description: (e as Error).message })
225
+ } finally {
226
+ setSyncing((s) => ({ ...s, [folder]: false }))
227
+ }
228
+ }
229
+
230
+ if (loading || !status) {
231
+ return (
232
+ <div className="flex items-center gap-2 px-3 py-1.5 text-xs text-muted-foreground">
233
+ <Loader2 className="h-3 w-3 animate-spin" />
234
+ Checking GitHub...
235
+ </div>
236
+ )
237
+ }
238
+
239
+ if (!status.ghInstalled) {
240
+ return (
241
+ <div className="space-y-2 px-3 py-1.5">
242
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
243
+ <CloudOff className="h-3.5 w-3.5" />
244
+ GitHub CLI not found
245
+ </div>
246
+ <a
247
+ href="https://cli.github.com"
248
+ target="_blank"
249
+ rel="noopener noreferrer"
250
+ className="block text-xs text-primary underline"
251
+ >
252
+ Install GitHub CLI
253
+ </a>
254
+ </div>
255
+ )
256
+ }
257
+
258
+ if (!status.ghAuthenticated) {
259
+ return (
260
+ <div className="space-y-2 px-3 py-1.5">
261
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
262
+ <CloudOff className="h-3.5 w-3.5" />
263
+ Not logged in to GitHub
264
+ </div>
265
+ <div className="rounded-md bg-muted px-2 py-1.5 text-xs font-mono">
266
+ gh auth login
267
+ </div>
268
+ </div>
269
+ )
270
+ }
271
+
272
+ const folders = [
273
+ { key: 'claude' as const, label: '.claude', status: status.claude },
274
+ { key: 'codex' as const, label: '.codex', status: status.codex },
275
+ ].filter((f) => f.status.exists)
276
+
277
+ return (
278
+ <div className="space-y-1.5 px-1">
279
+ <div className="flex items-center gap-1.5 px-2 text-xs text-muted-foreground">
280
+ <Cloud className="h-3 w-3" />
281
+ {status.ghUser}
282
+ </div>
283
+ {folders.map(({ key, label, status: fs }) => {
284
+ const isSetUp = fs.hasGit && fs.hasRemote
285
+ const isSyncing = syncing[key]
286
+
287
+ return (
288
+ <div key={key} className="flex items-center justify-between px-2">
289
+ <div className="flex items-center gap-1.5">
290
+ <span className="text-xs font-medium">~/{label}</span>
291
+ {isSetUp && !fs.isDirty && (
292
+ <Check className="h-3 w-3 text-green-500" />
293
+ )}
294
+ {isSetUp && fs.isDirty && (
295
+ <Badge variant="secondary" className="h-4 px-1 text-[10px]">
296
+ new
297
+ </Badge>
298
+ )}
299
+ </div>
300
+ {!isSetUp ? (
301
+ <Button
302
+ variant="outline"
303
+ size="xs"
304
+ onClick={() => handleInit(key)}
305
+ disabled={isSyncing}
306
+ className="gap-1"
307
+ >
308
+ {isSyncing ? <Loader2 className="h-3 w-3 animate-spin" /> : <Upload className="h-3 w-3" />}
309
+ Backup
310
+ </Button>
311
+ ) : (
312
+ <Button
313
+ variant="ghost"
314
+ size="xs"
315
+ onClick={() => handleSync(key)}
316
+ disabled={isSyncing}
317
+ className="gap-1"
318
+ >
319
+ {isSyncing ? <Loader2 className="h-3 w-3 animate-spin" /> : <Cloud className="h-3 w-3" />}
320
+ Sync
321
+ </Button>
322
+ )}
323
+ </div>
324
+ )
325
+ })}
326
+ </div>
327
+ )
328
+ }
329
+
109
330
  export function AppSidebar() {
110
331
  const pathname = usePathname()
111
332
  const plugins = getPlugins()
@@ -115,10 +336,7 @@ export function AppSidebar() {
115
336
  <SidebarHeader>
116
337
  <div className="flex items-center gap-2 px-2 py-2">
117
338
  <img src="/logo.svg" alt="AgentFit" className="h-8 w-8" />
118
- <div>
119
- <div className="text-sm font-semibold">AgentFit</div>
120
- <div className="text-xs text-muted-foreground">Coding Agent Fitness Tracker</div>
121
- </div>
339
+ <div className="text-sm font-semibold">AgentFit</div>
122
340
  </div>
123
341
  </SidebarHeader>
124
342
  <SidebarContent>
@@ -154,7 +372,39 @@ export function AppSidebar() {
154
372
  </SidebarMenu>
155
373
  </SidebarGroupContent>
156
374
  </SidebarGroup>
375
+
376
+ {/* Data Management Section */}
377
+ <SidebarGroup>
378
+ <SidebarGroupLabel className="sr-only">Settings</SidebarGroupLabel>
379
+ <SidebarGroupContent>
380
+ <SidebarMenu>
381
+ <SidebarMenuItem>
382
+ <SidebarMenuButton
383
+ render={<Link href="/settings" />}
384
+ isActive={pathname === '/settings'}
385
+ >
386
+ <Settings className="h-4 w-4" />
387
+ <span>Settings</span>
388
+ </SidebarMenuButton>
389
+ </SidebarMenuItem>
390
+ </SidebarMenu>
391
+ </SidebarGroupContent>
392
+ </SidebarGroup>
157
393
  </SidebarContent>
394
+
395
+ <SidebarFooter>
396
+ <div className="flex items-center justify-between px-3 py-2 text-xs text-muted-foreground">
397
+ <span>v0.1.0</span>
398
+ <a
399
+ href="https://github.com/harrywang/agentfit"
400
+ target="_blank"
401
+ rel="noopener noreferrer"
402
+ className="hover:text-foreground transition-colors"
403
+ >
404
+ GitHub
405
+ </a>
406
+ </div>
407
+ </SidebarFooter>
158
408
  <SidebarRail />
159
409
  </Sidebar>
160
410
  )
@@ -0,0 +1,236 @@
1
+ 'use client'
2
+
3
+ import { useState, useEffect, useCallback } from 'react'
4
+ import { toast } from 'sonner'
5
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
6
+ import { Button } from '@/components/ui/button'
7
+ import { Badge } from '@/components/ui/badge'
8
+ import {
9
+ Cloud,
10
+ CloudOff,
11
+ Check,
12
+ Loader2,
13
+ Upload,
14
+ } from 'lucide-react'
15
+
16
+ interface FolderStatus {
17
+ exists: boolean
18
+ hasGit: boolean
19
+ hasRemote: boolean
20
+ isDirty: boolean
21
+ }
22
+
23
+ interface BackupStatus {
24
+ ghInstalled: boolean
25
+ ghAuthenticated: boolean
26
+ ghUser: string
27
+ claude: FolderStatus
28
+ codex: FolderStatus
29
+ }
30
+
31
+ export function BackupCard() {
32
+ const [status, setStatus] = useState<BackupStatus | null>(null)
33
+ const [loading, setLoading] = useState(false)
34
+ const [syncing, setSyncing] = useState<Record<string, boolean>>({})
35
+
36
+ const fetchStatus = useCallback(async () => {
37
+ setLoading(true)
38
+ try {
39
+ const res = await fetch('/api/backup')
40
+ setStatus(await res.json())
41
+ } catch {
42
+ // ignore
43
+ } finally {
44
+ setLoading(false)
45
+ }
46
+ }, [])
47
+
48
+ useEffect(() => {
49
+ fetchStatus()
50
+ }, [fetchStatus])
51
+
52
+ const handleInit = async (folder: 'claude' | 'codex') => {
53
+ setSyncing((s) => ({ ...s, [folder]: true }))
54
+ try {
55
+ const res = await fetch('/api/backup', {
56
+ method: 'POST',
57
+ headers: { 'Content-Type': 'application/json' },
58
+ body: JSON.stringify({ action: 'init', folder }),
59
+ })
60
+ const data = await res.json()
61
+ if (!res.ok) {
62
+ toast.error('Backup failed', { description: data.error })
63
+ return
64
+ }
65
+ toast.success(`~/.${folder} backed up`, {
66
+ description: (
67
+ <a href={data.repoUrl} target="_blank" rel="noopener noreferrer" className="underline">
68
+ {data.repoUrl}
69
+ </a>
70
+ ),
71
+ })
72
+ await fetchStatus()
73
+ } catch (e) {
74
+ toast.error('Backup failed', { description: (e as Error).message })
75
+ } finally {
76
+ setSyncing((s) => ({ ...s, [folder]: false }))
77
+ }
78
+ }
79
+
80
+ const handleSync = async (folder: 'claude' | 'codex') => {
81
+ setSyncing((s) => ({ ...s, [folder]: true }))
82
+ try {
83
+ const res = await fetch('/api/backup', {
84
+ method: 'POST',
85
+ headers: { 'Content-Type': 'application/json' },
86
+ body: JSON.stringify({ action: 'sync', folder }),
87
+ })
88
+ const data = await res.json()
89
+ if (!res.ok) {
90
+ toast.error('Sync failed', { description: data.error })
91
+ return
92
+ }
93
+ toast.success(`~/.${folder} synced`, { description: data.message })
94
+ await fetchStatus()
95
+ } catch (e) {
96
+ toast.error('Sync failed', { description: (e as Error).message })
97
+ } finally {
98
+ setSyncing((s) => ({ ...s, [folder]: false }))
99
+ }
100
+ }
101
+
102
+ if (loading) {
103
+ return (
104
+ <Card>
105
+ <CardHeader>
106
+ <div className="flex items-center gap-2">
107
+ <Cloud className="h-5 w-5 text-muted-foreground" />
108
+ <div>
109
+ <CardTitle>GitHub Backup</CardTitle>
110
+ <CardDescription>Checking GitHub status...</CardDescription>
111
+ </div>
112
+ </div>
113
+ </CardHeader>
114
+ <CardContent>
115
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
116
+ <Loader2 className="h-4 w-4 animate-spin" />
117
+ Loading...
118
+ </div>
119
+ </CardContent>
120
+ </Card>
121
+ )
122
+ }
123
+
124
+ if (!status || !status.ghInstalled) {
125
+ return (
126
+ <Card>
127
+ <CardHeader>
128
+ <div className="flex items-center gap-2">
129
+ <CloudOff className="h-5 w-5 text-muted-foreground" />
130
+ <div>
131
+ <CardTitle>GitHub Backup</CardTitle>
132
+ <CardDescription>Back up your agent logs to a private GitHub repo</CardDescription>
133
+ </div>
134
+ </div>
135
+ </CardHeader>
136
+ <CardContent>
137
+ <div className="space-y-2 text-sm text-muted-foreground">
138
+ <p>GitHub CLI is not installed.</p>
139
+ <a href="https://cli.github.com" target="_blank" rel="noopener noreferrer" className="text-primary underline">
140
+ Install GitHub CLI
141
+ </a>
142
+ </div>
143
+ </CardContent>
144
+ </Card>
145
+ )
146
+ }
147
+
148
+ if (!status.ghAuthenticated) {
149
+ return (
150
+ <Card>
151
+ <CardHeader>
152
+ <div className="flex items-center gap-2">
153
+ <CloudOff className="h-5 w-5 text-muted-foreground" />
154
+ <div>
155
+ <CardTitle>GitHub Backup</CardTitle>
156
+ <CardDescription>Back up your agent logs to a private GitHub repo</CardDescription>
157
+ </div>
158
+ </div>
159
+ </CardHeader>
160
+ <CardContent>
161
+ <div className="space-y-2 text-sm text-muted-foreground">
162
+ <p>Not logged in to GitHub.</p>
163
+ <code className="block rounded bg-muted px-2 py-1.5 text-xs font-mono">gh auth login</code>
164
+ </div>
165
+ </CardContent>
166
+ </Card>
167
+ )
168
+ }
169
+
170
+ const folders = [
171
+ { key: 'claude' as const, label: '~/.claude', status: status.claude },
172
+ { key: 'codex' as const, label: '~/.codex', status: status.codex },
173
+ ].filter((f) => f.status.exists)
174
+
175
+ return (
176
+ <Card>
177
+ <CardHeader>
178
+ <div className="flex items-center gap-2">
179
+ <Cloud className="h-5 w-5 text-muted-foreground" />
180
+ <div>
181
+ <CardTitle>GitHub Backup</CardTitle>
182
+ <CardDescription>
183
+ Back up agent logs to private GitHub repos — logged in as <strong>{status.ghUser}</strong>
184
+ </CardDescription>
185
+ </div>
186
+ </div>
187
+ </CardHeader>
188
+ <CardContent className="space-y-3">
189
+ {folders.map(({ key, label, status: fs }) => {
190
+ const isSetUp = fs.hasGit && fs.hasRemote
191
+ const isSyncing = syncing[key]
192
+
193
+ return (
194
+ <div key={key} className="flex items-center justify-between rounded-lg border p-4">
195
+ <div className="flex items-center gap-3">
196
+ <code className="text-sm font-medium">{label}</code>
197
+ {isSetUp && !fs.isDirty && (
198
+ <Check className="h-4 w-4 text-green-500" />
199
+ )}
200
+ {isSetUp && fs.isDirty && (
201
+ <Badge variant="secondary" className="text-xs">new changes</Badge>
202
+ )}
203
+ </div>
204
+ {!isSetUp ? (
205
+ <Button
206
+ variant="outline"
207
+ size="sm"
208
+ onClick={() => handleInit(key)}
209
+ disabled={isSyncing}
210
+ className="gap-2"
211
+ >
212
+ {isSyncing ? <Loader2 className="h-4 w-4 animate-spin" /> : <Upload className="h-4 w-4" />}
213
+ Backup
214
+ </Button>
215
+ ) : (
216
+ <Button
217
+ variant="outline"
218
+ size="sm"
219
+ onClick={() => handleSync(key)}
220
+ disabled={isSyncing}
221
+ className="gap-2"
222
+ >
223
+ {isSyncing ? <Loader2 className="h-4 w-4 animate-spin" /> : <Cloud className="h-4 w-4" />}
224
+ Sync
225
+ </Button>
226
+ )}
227
+ </div>
228
+ )
229
+ })}
230
+ {folders.length === 0 && (
231
+ <p className="text-sm text-muted-foreground">No agent log directories found.</p>
232
+ )}
233
+ </CardContent>
234
+ </Card>
235
+ )
236
+ }