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
package/README.md CHANGED
@@ -10,56 +10,64 @@ Fitness tracker dashboard for AI coding agents. Reads your local Claude Code and
10
10
  curl -fsSL https://raw.githubusercontent.com/harrywang/agentfit/main/setup.sh | bash
11
11
  ```
12
12
 
13
- Then start the dashboard:
14
-
15
- ```bash
16
- cd agentfit && npm start
17
- ```
18
-
19
13
  ### Option 2: npx
20
14
 
21
15
  ```bash
22
16
  npx agentfit
23
17
  ```
24
18
 
25
- This handles first-run setup (database creation, build) automatically.
26
-
27
19
  ### Option 3: Manual
28
20
 
29
21
  ```bash
30
22
  git clone https://github.com/harrywang/agentfit.git
31
23
  cd agentfit
32
24
  npm install
33
- echo 'DATABASE_URL="file:./dev.db"' > .env
25
+ echo 'DATABASE_URL="file:./agentfit.db"' > .env
34
26
  npx prisma migrate deploy
35
27
  npm run build
36
28
  npm start
37
29
  ```
38
30
 
39
- Open [http://localhost:3000](http://localhost:3000). The app auto-syncs your `~/.claude/projects/` logs on first load.
31
+ Open [http://localhost:3000](http://localhost:3000). The app auto-syncs your Claude Code (`~/.claude/projects/`) and Codex (`~/.codex/sessions/`) logs on first load.
40
32
 
41
33
  **Requirements:** Node.js 20+
42
34
 
35
+ ## The CRAFT Framework
36
+
37
+ AgentFit scores your AI coding proficiency using **CRAFT** — a Human-AI coding proficiency framework by [Harry Wang](https://harrywang.me). All metrics are computed from your local conversation logs.
38
+
39
+ | | Dimension | What it measures | Key metrics |
40
+ |---|---|---|---|
41
+ | **C** | **Context** | How effectively you engineer context for the AI | CLAUDE.md usage, memory writes, cache hit rate |
42
+ | **R** | **Reach** | How broadly you leverage available capabilities | Tool diversity, subagent usage, skill adoption |
43
+ | **A** | **Autonomy** | How independently the agent works for you | Message ratio, interruption rate, delegation |
44
+ | **F** | **Flow** | How consistently you maintain a coding rhythm | Streak length, daily consistency, active days |
45
+ | **T** | **Throughput** | How much output you get for your investment | Cost efficiency, output volume, error rate |
46
+
47
+ Inspired by [DORA Metrics](https://dora.dev) and Microsoft's [SPACE framework](https://queue.acm.org/detail.cfm?id=3454124). Behavioral signals from your logs — no surveys, no guesswork. Each dimension is scored 0–100.
48
+
43
49
  ## Features
44
50
 
45
51
  - **Dashboard** — overview stats (cost, tokens, sessions, projects, messages, tool calls, duration)
46
- - **Agent Coach** — fitness score, achievements, improvement areas, and tips
52
+ - **CRAFT Coach** — fitness score, achievements, and actionable improvement tips
47
53
  - **Daily Usage** — daily cost and activity charts
48
54
  - **Token Breakdown** — pie chart + stacked area chart of token types
49
- - **Tool Usage** — top 20 tools by invocation count
55
+ - **Tool Usage** — top tools by invocation count
50
56
  - **Projects** — per-project breakdown with top tools and sessions
51
- - **Personality Fit** — MBTI-style behavioral analysis with task fit scores
57
+ - **Sessions** — individual session details with chat logs and tool flow graphs
58
+ - **Personality Fit** — MBTI-style behavioral analysis
52
59
  - **Command Usage** — slash command pattern tracking
53
60
  - **Images** — screenshot analysis across sessions
54
- - **Community Plugins** — extensible analysis views contributed by the community
61
+ - **Community Plugins** — extensible analysis views
55
62
 
56
- ## Data
63
+ ## Desktop App
57
64
 
58
- - Session metrics stored in local SQLite (`dev.db`) via Prisma
59
- - Images extracted from conversation logs to `data/images/`
60
- - Pricing fetched from [LiteLLM](https://github.com/BerriAI/litellm) model pricing database
61
- - Incremental sync only new sessions are imported on each sync
62
- - Supports Claude Code, Codex, or combined views
65
+ Download a pre-built installer from the [Releases](https://github.com/harrywang/agentfit/releases) page, or build yourself:
66
+
67
+ ```bash
68
+ npm run electron:build:mac # Mac (.dmg)
69
+ npm run electron:build:win # Windows (.exe)
70
+ ```
63
71
 
64
72
  ## Development
65
73
 
@@ -70,23 +78,11 @@ npm run lint # ESLint
70
78
  npm run format # Prettier
71
79
  npm run typecheck # TypeScript check
72
80
  npm test # Run tests
73
- npm run test:watch # Tests in watch mode
74
- ```
75
-
76
- ### Database
77
-
78
- ```bash
79
- npx prisma migrate dev --name <name> # Create/apply migration
80
- npx prisma generate # Regenerate Prisma client
81
81
  ```
82
82
 
83
83
  ## Community Plugins
84
84
 
85
- AgentFit has a plugin system for community-contributed analysis views. Plugins appear in the **Community** section of the sidebar.
86
-
87
- See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide on creating your own plugin.
88
-
89
- **Quick version:**
85
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide. Quick version:
90
86
 
91
87
  1. Create `plugins/<your-slug>/manifest.ts` and `component.tsx`
92
88
  2. Register in `plugins/index.ts`
@@ -98,7 +94,7 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide on creating your own p
98
94
  | Variable | Default | Description |
99
95
  |----------|---------|-------------|
100
96
  | `PORT` or `AGENTFIT_PORT` | `3000` | Server port |
101
- | `DATABASE_URL` | `file:./dev.db` | SQLite database path |
97
+ | `DATABASE_URL` | `file:./agentfit.db` | SQLite database path |
102
98
 
103
99
  ## Credits
104
100
 
@@ -11,7 +11,7 @@ export default function DailyPage() {
11
11
  return (
12
12
  <>
13
13
  <div className="grid gap-4 lg:grid-cols-3">
14
- <DailyChart daily={data.daily} />
14
+ <DailyChart daily={data.daily} sessions={data.sessions} />
15
15
  </div>
16
16
  <DailyTable daily={data.daily} sessions={data.sessions} />
17
17
  </>
@@ -0,0 +1,17 @@
1
+ 'use client'
2
+
3
+ import { useData } from '@/components/data-provider'
4
+ import { ToolFlowGraph } from '@/components/tool-flow-graph'
5
+ import { SessionTimeline } from '@/components/session-timeline'
6
+
7
+ export default function FlowPage() {
8
+ const { data } = useData()
9
+ if (!data) return null
10
+
11
+ return (
12
+ <>
13
+ <SessionTimeline sessions={data.sessions} />
14
+ <ToolFlowGraph data={data} />
15
+ </>
16
+ )
17
+ }
@@ -2,11 +2,13 @@
2
2
 
3
3
  import { DataProvider } from '@/components/data-provider'
4
4
  import { DashboardShell } from '@/components/dashboard-shell'
5
+ import { Toaster } from '@/components/ui/sonner'
5
6
 
6
7
  export default function DashboardLayout({ children }: { children: React.ReactNode }) {
7
8
  return (
8
9
  <DataProvider>
9
10
  <DashboardShell>{children}</DashboardShell>
11
+ <Toaster />
10
12
  </DataProvider>
11
13
  )
12
14
  }
@@ -3,21 +3,40 @@
3
3
  import { useData } from '@/components/data-provider'
4
4
  import { FitnessScore } from '@/components/fitness-score'
5
5
  import { OverviewCards } from '@/components/overview-cards'
6
- import { DailyChart } from '@/components/daily-chart'
7
- import { ToolUsageChart } from '@/components/tool-usage-chart'
6
+ import { DailyCostChart, TopCommandsChart, TokenUsageHeatmap, UserVsAssistantChart, InterruptionRateChart, ToolMixChart } from '@/components/daily-chart'
7
+ import { RefreshCw } from 'lucide-react'
8
8
 
9
9
  export default function DashboardPage() {
10
- const { data } = useData()
10
+ const { data, newSessionsAvailable, handleSync, syncing } = useData()
11
11
  if (!data) return null
12
12
 
13
13
  return (
14
14
  <>
15
+ {newSessionsAvailable > 0 && (
16
+ <div className="flex items-center justify-between rounded-lg border border-blue-200 bg-blue-50 px-4 py-3 text-sm dark:border-blue-800 dark:bg-blue-950">
17
+ <span>
18
+ <strong>{newSessionsAvailable}</strong> new session{newSessionsAvailable !== 1 ? 's' : ''} detected on disk. Sync to update your dashboard.
19
+ </span>
20
+ <button
21
+ onClick={handleSync}
22
+ disabled={syncing}
23
+ className="inline-flex items-center gap-1.5 rounded-md bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700 disabled:opacity-50"
24
+ >
25
+ <RefreshCw className={`h-3 w-3 ${syncing ? 'animate-spin' : ''}`} />
26
+ {syncing ? 'Syncing…' : 'Sync now'}
27
+ </button>
28
+ </div>
29
+ )}
15
30
  <FitnessScore data={data} />
16
31
  <OverviewCards overview={data.overview} sessions={data.sessions} />
17
32
  <div className="grid gap-4 lg:grid-cols-3">
18
- <DailyChart daily={data.daily} />
33
+ <DailyCostChart daily={data.daily} />
34
+ <TopCommandsChart />
35
+ <TokenUsageHeatmap daily={data.daily} />
36
+ <UserVsAssistantChart daily={data.daily} />
37
+ <InterruptionRateChart daily={data.daily} />
38
+ <ToolMixChart daily={data.daily} />
19
39
  </div>
20
- <ToolUsageChart toolUsage={data.toolUsage} />
21
40
  </>
22
41
  )
23
42
  }
@@ -0,0 +1,72 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { useParams } from 'next/navigation'
5
+ import Link from 'next/link'
6
+ import { ArrowLeft, Loader2 } from 'lucide-react'
7
+ import { ReportView } from '@/components/report-view'
8
+ import type { ReportContent } from '@/lib/report'
9
+
10
+ interface ReportData {
11
+ id: string
12
+ title: string
13
+ generatedAt: string
14
+ contentJson: ReportContent
15
+ sessionCount: number
16
+ }
17
+
18
+ export default function ReportDetailPage() {
19
+ const params = useParams()
20
+ const id = params.id as string
21
+ const [report, setReport] = useState<ReportData | null>(null)
22
+ const [loading, setLoading] = useState(true)
23
+ const [error, setError] = useState<string | null>(null)
24
+
25
+ useEffect(() => {
26
+ async function load() {
27
+ try {
28
+ const res = await fetch(`/api/reports/${id}`)
29
+ if (!res.ok) throw new Error('Report not found')
30
+ setReport(await res.json())
31
+ } catch (e) {
32
+ setError((e as Error).message)
33
+ } finally {
34
+ setLoading(false)
35
+ }
36
+ }
37
+ load()
38
+ }, [id])
39
+
40
+ if (loading) {
41
+ return (
42
+ <div className="flex items-center gap-2 py-8 text-muted-foreground">
43
+ <Loader2 className="h-4 w-4 animate-spin" /> Loading report...
44
+ </div>
45
+ )
46
+ }
47
+
48
+ if (error || !report) {
49
+ return (
50
+ <div className="space-y-4">
51
+ <Link href="/reports" className="flex items-center gap-1 text-sm text-primary hover:underline">
52
+ <ArrowLeft className="h-4 w-4" /> Back to reports
53
+ </Link>
54
+ <div className="text-destructive">{error || 'Report not found'}</div>
55
+ </div>
56
+ )
57
+ }
58
+
59
+ return (
60
+ <div className="space-y-4">
61
+ <div className="flex items-center justify-between">
62
+ <Link href="/reports" className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
63
+ <ArrowLeft className="h-4 w-4" /> Back to reports
64
+ </Link>
65
+ <span className="text-xs text-muted-foreground">
66
+ Generated {new Date(report.generatedAt).toLocaleString()} · {report.sessionCount} sessions
67
+ </span>
68
+ </div>
69
+ <ReportView content={report.contentJson} />
70
+ </div>
71
+ )
72
+ }
@@ -0,0 +1,132 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import Link from 'next/link'
5
+ import { Card, CardContent } from '@/components/ui/card'
6
+ import { Button } from '@/components/ui/button'
7
+ import { Badge } from '@/components/ui/badge'
8
+ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
9
+ import { FileText, Plus, Loader2, ChevronRight } from 'lucide-react'
10
+ import { toast } from 'sonner'
11
+ import { useData } from '@/components/data-provider'
12
+
13
+ interface ReportSummary {
14
+ id: string
15
+ title: string
16
+ generatedAt: string
17
+ sessionCount: number
18
+ }
19
+
20
+ export default function ReportsPage() {
21
+ const { data } = useData()
22
+ const [reports, setReports] = useState<ReportSummary[]>([])
23
+ const [loading, setLoading] = useState(true)
24
+ const [generating, setGenerating] = useState(false)
25
+
26
+ async function fetchReports() {
27
+ try {
28
+ const res = await fetch('/api/reports')
29
+ setReports(await res.json())
30
+ } catch {
31
+ // ignore
32
+ } finally {
33
+ setLoading(false)
34
+ }
35
+ }
36
+
37
+ useEffect(() => { fetchReports() }, [])
38
+
39
+ const currentSessionCount = data?.overview.totalSessions || 0
40
+ const lastReportSessionCount = reports[0]?.sessionCount || 0
41
+ const hasNewData = reports.length === 0 || currentSessionCount !== lastReportSessionCount
42
+
43
+ async function handleGenerate() {
44
+ setGenerating(true)
45
+ try {
46
+ const res = await fetch('/api/reports', { method: 'POST' })
47
+ const report = await res.json()
48
+ if (res.status === 409) {
49
+ toast.info('No new data', { description: report.error })
50
+ return
51
+ }
52
+ if (!res.ok) throw new Error(report.error || 'Failed to generate report')
53
+ toast.success('Report generated', { description: report.title })
54
+ await fetchReports()
55
+ } catch (e) {
56
+ toast.error('Failed to generate report', { description: (e as Error).message })
57
+ } finally {
58
+ setGenerating(false)
59
+ }
60
+ }
61
+
62
+ const buttonDisabled = generating || !hasNewData
63
+
64
+ return (
65
+ <div className="space-y-6 max-w-3xl">
66
+ <div className="flex items-center justify-between">
67
+ <div>
68
+ <h2 className="text-lg font-semibold">Insights Reports</h2>
69
+ <p className="text-sm text-muted-foreground">
70
+ Generate point-in-time snapshots of your coding agent fitness
71
+ </p>
72
+ </div>
73
+ <Tooltip>
74
+ <TooltipTrigger
75
+ render={<span />}
76
+ >
77
+ <Button onClick={handleGenerate} disabled={buttonDisabled} className="gap-2">
78
+ {generating ? <Loader2 className="h-4 w-4 animate-spin" /> : <Plus className="h-4 w-4" />}
79
+ {generating ? 'Generating...' : 'Generate Report'}
80
+ </Button>
81
+ </TooltipTrigger>
82
+ {!hasNewData && (
83
+ <TooltipContent>
84
+ No new sessions since last report. Sync new data first.
85
+ </TooltipContent>
86
+ )}
87
+ </Tooltip>
88
+ </div>
89
+
90
+ {loading ? (
91
+ <div className="flex items-center gap-2 py-8 text-muted-foreground">
92
+ <Loader2 className="h-4 w-4 animate-spin" /> Loading reports...
93
+ </div>
94
+ ) : reports.length === 0 ? (
95
+ <Card>
96
+ <CardContent className="py-12 text-center">
97
+ <FileText className="h-10 w-10 text-muted-foreground/30 mx-auto mb-3" />
98
+ <p className="text-sm text-muted-foreground">
99
+ No reports yet. Click "Generate Report" to create your first fitness snapshot.
100
+ </p>
101
+ </CardContent>
102
+ </Card>
103
+ ) : (
104
+ <div className="space-y-2">
105
+ {reports.map(report => (
106
+ <Link key={report.id} href={`/reports/${report.id}`}>
107
+ <Card className="hover:bg-muted/50 transition-colors cursor-pointer">
108
+ <CardContent className="flex items-center justify-between py-4">
109
+ <div className="flex items-center gap-3">
110
+ <FileText className="h-5 w-5 text-muted-foreground" />
111
+ <div>
112
+ <div className="text-sm font-medium">{report.title}</div>
113
+ <div className="text-xs text-muted-foreground">
114
+ {new Date(report.generatedAt).toLocaleString()}
115
+ </div>
116
+ </div>
117
+ </div>
118
+ <div className="flex items-center gap-3">
119
+ <Badge variant="secondary" className="text-xs">
120
+ {report.sessionCount} sessions
121
+ </Badge>
122
+ <ChevronRight className="h-4 w-4 text-muted-foreground" />
123
+ </div>
124
+ </CardContent>
125
+ </Card>
126
+ </Link>
127
+ ))}
128
+ </div>
129
+ )}
130
+ </div>
131
+ )
132
+ }
@@ -0,0 +1,167 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { useParams } from 'next/navigation'
5
+ import Link from 'next/link'
6
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
7
+ import { ArrowLeft, Clock, Coins, MessageSquare, Wrench, CheckCircle, XCircle } from 'lucide-react'
8
+ import { SessionWorkflow } from '@/components/session-workflow'
9
+ import { SessionChatLog } from '@/components/session-chatlog'
10
+ import type { SessionDetail } from '@/lib/session-detail'
11
+
12
+ export default function SessionDetailPage() {
13
+ const params = useParams()
14
+ const sessionId = params.id as string
15
+ const [detail, setDetail] = useState<SessionDetail | null>(null)
16
+ const [loading, setLoading] = useState(true)
17
+ const [error, setError] = useState<string | null>(null)
18
+ const [activeTab, setActiveTab] = useState<'workflow' | 'dialog'>('dialog')
19
+
20
+ useEffect(() => {
21
+ async function load() {
22
+ try {
23
+ const res = await fetch(`/api/session?id=${sessionId}`)
24
+ if (!res.ok) throw new Error('Session not found')
25
+ const data = await res.json()
26
+ setDetail(data)
27
+ } catch (e) {
28
+ setError((e as Error).message)
29
+ } finally {
30
+ setLoading(false)
31
+ }
32
+ }
33
+ load()
34
+ }, [sessionId])
35
+
36
+ if (loading) {
37
+ return <div className="text-muted-foreground">Loading session...</div>
38
+ }
39
+
40
+ if (error || !detail) {
41
+ return (
42
+ <div className="space-y-4">
43
+ <Link href="/sessions" className="flex items-center gap-1 text-sm text-primary hover:underline">
44
+ <ArrowLeft className="h-4 w-4" /> Back to sessions
45
+ </Link>
46
+ <div className="text-destructive">{error || 'Session not found'}</div>
47
+ </div>
48
+ )
49
+ }
50
+
51
+ const s = detail.stats
52
+
53
+ return (
54
+ <div className="space-y-4">
55
+ <Link href="/sessions" className="flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground">
56
+ <ArrowLeft className="h-4 w-4" /> Back to sessions
57
+ </Link>
58
+
59
+ <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-6">
60
+ <Card>
61
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
62
+ <CardTitle className="text-sm font-medium text-muted-foreground">Duration</CardTitle>
63
+ <Clock className="h-4 w-4 text-muted-foreground" />
64
+ </CardHeader>
65
+ <CardContent>
66
+ <div className="text-2xl font-bold tracking-tight">{s.duration}</div>
67
+ </CardContent>
68
+ </Card>
69
+ <Card>
70
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
71
+ <CardTitle className="text-sm font-medium text-muted-foreground">Tokens</CardTitle>
72
+ <Coins className="h-4 w-4 text-muted-foreground" />
73
+ </CardHeader>
74
+ <CardContent>
75
+ <div className="text-2xl font-bold tracking-tight">{s.tokens.toLocaleString()}</div>
76
+ </CardContent>
77
+ </Card>
78
+ <Card>
79
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
80
+ <CardTitle className="text-sm font-medium text-muted-foreground">Messages</CardTitle>
81
+ <MessageSquare className="h-4 w-4 text-muted-foreground" />
82
+ </CardHeader>
83
+ <CardContent>
84
+ <div className="text-2xl font-bold tracking-tight">{s.totalMessages}</div>
85
+ <p className="text-xs text-muted-foreground">{s.userTurns} user / {s.assistantTurns} assistant</p>
86
+ </CardContent>
87
+ </Card>
88
+ <Card>
89
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
90
+ <CardTitle className="text-sm font-medium text-muted-foreground">Tool Calls</CardTitle>
91
+ <Wrench className="h-4 w-4 text-muted-foreground" />
92
+ </CardHeader>
93
+ <CardContent>
94
+ <div className="text-2xl font-bold tracking-tight">{s.toolCalls}</div>
95
+ </CardContent>
96
+ </Card>
97
+ <Card>
98
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
99
+ <CardTitle className="text-sm font-medium text-muted-foreground">Success</CardTitle>
100
+ <CheckCircle className="h-4 w-4 text-muted-foreground" />
101
+ </CardHeader>
102
+ <CardContent>
103
+ <div className="text-2xl font-bold tracking-tight">{s.successCount}</div>
104
+ </CardContent>
105
+ </Card>
106
+ <Card>
107
+ <CardHeader className="flex flex-row items-center justify-between pb-2">
108
+ <CardTitle className="text-sm font-medium text-muted-foreground">Failures</CardTitle>
109
+ <XCircle className="h-4 w-4 text-muted-foreground" />
110
+ </CardHeader>
111
+ <CardContent>
112
+ <div className={`text-2xl font-bold tracking-tight ${s.failureCount > 0 ? 'text-destructive' : ''}`}>{s.failureCount}</div>
113
+ </CardContent>
114
+ </Card>
115
+ </div>
116
+
117
+ <div className="flex gap-1.5 border-b pb-1">
118
+ <button
119
+ onClick={() => setActiveTab('dialog')}
120
+ className={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
121
+ activeTab === 'dialog'
122
+ ? 'bg-primary text-primary-foreground'
123
+ : 'text-muted-foreground hover:text-foreground'
124
+ }`}
125
+ >
126
+ Dialog
127
+ </button>
128
+ <button
129
+ onClick={() => setActiveTab('workflow')}
130
+ className={`rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${
131
+ activeTab === 'workflow'
132
+ ? 'bg-primary text-primary-foreground'
133
+ : 'text-muted-foreground hover:text-foreground'
134
+ }`}
135
+ >
136
+ Workflow
137
+ </button>
138
+ </div>
139
+
140
+ {activeTab === 'dialog' && (
141
+ <Card>
142
+ <CardHeader>
143
+ <CardTitle>Chat Log</CardTitle>
144
+ <CardDescription>{detail.chatLog.length} records</CardDescription>
145
+ </CardHeader>
146
+ <CardContent>
147
+ <SessionChatLog messages={detail.chatLog} />
148
+ </CardContent>
149
+ </Card>
150
+ )}
151
+
152
+ {activeTab === 'workflow' && (
153
+ <Card>
154
+ <CardHeader>
155
+ <CardTitle>Workflow</CardTitle>
156
+ <CardDescription>
157
+ Tool calls, reasoning, and execution flow — {detail.workflowNodes.length} nodes
158
+ </CardDescription>
159
+ </CardHeader>
160
+ <CardContent>
161
+ <SessionWorkflow nodes={detail.workflowNodes} edges={detail.workflowEdges} />
162
+ </CardContent>
163
+ </Card>
164
+ )}
165
+ </div>
166
+ )
167
+ }