agentfit 0.1.0 → 0.1.2

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 (74) hide show
  1. package/.github/workflows/release.yml +111 -0
  2. package/README.md +41 -38
  3. package/app/(dashboard)/daily/page.tsx +1 -1
  4. package/app/(dashboard)/data-management/page.tsx +180 -0
  5. package/app/(dashboard)/flow/page.tsx +17 -0
  6. package/app/(dashboard)/layout.tsx +2 -0
  7. package/app/(dashboard)/page.tsx +24 -5
  8. package/app/(dashboard)/reports/[id]/page.tsx +72 -0
  9. package/app/(dashboard)/reports/page.tsx +132 -0
  10. package/app/(dashboard)/sessions/[id]/page.tsx +167 -0
  11. package/app/api/backup/route.ts +215 -0
  12. package/app/api/check/route.ts +11 -1
  13. package/app/api/command-insights/route.ts +13 -0
  14. package/app/api/commands/route.ts +55 -1
  15. package/app/api/images-analysis/route.ts +3 -4
  16. package/app/api/reports/[id]/route.ts +23 -0
  17. package/app/api/reports/route.ts +50 -0
  18. package/app/api/reset/route.ts +21 -0
  19. package/app/api/session/route.ts +40 -0
  20. package/app/api/usage/route.ts +26 -1
  21. package/app/layout.tsx +1 -1
  22. package/bin/agentfit.mjs +2 -2
  23. package/components/agent-coach.tsx +256 -129
  24. package/components/app-sidebar.tsx +45 -10
  25. package/components/backup-section.tsx +236 -0
  26. package/components/daily-chart.tsx +447 -83
  27. package/components/dashboard-shell.tsx +29 -31
  28. package/components/data-provider.tsx +88 -8
  29. package/components/fitness-score.tsx +95 -54
  30. package/components/overview-cards.tsx +148 -41
  31. package/components/report-view.tsx +307 -0
  32. package/components/screenshots-analysis.tsx +51 -46
  33. package/components/session-chatlog.tsx +124 -0
  34. package/components/session-timeline.tsx +184 -0
  35. package/components/session-workflow.tsx +183 -0
  36. package/components/sessions-table.tsx +9 -1
  37. package/components/tool-flow-graph.tsx +144 -0
  38. package/components/ui/carousel.tsx +242 -0
  39. package/components/ui/sidebar.tsx +1 -1
  40. package/components/ui/sonner.tsx +51 -0
  41. package/electron/entitlements.mac.plist +16 -0
  42. package/electron/init-db.mjs +37 -0
  43. package/electron/main.mjs +203 -0
  44. package/generated/prisma/browser.ts +5 -0
  45. package/generated/prisma/client.ts +5 -0
  46. package/generated/prisma/internal/class.ts +14 -4
  47. package/generated/prisma/internal/prismaNamespace.ts +97 -2
  48. package/generated/prisma/internal/prismaNamespaceBrowser.ts +21 -1
  49. package/generated/prisma/models/Report.ts +1219 -0
  50. package/generated/prisma/models/Session.ts +221 -1
  51. package/generated/prisma/models.ts +1 -0
  52. package/lib/coach.ts +571 -211
  53. package/lib/command-insights.ts +231 -0
  54. package/lib/db.ts +2 -2
  55. package/lib/parse-codex.ts +6 -0
  56. package/lib/parse-logs.ts +80 -1
  57. package/lib/queries-codex.ts +24 -0
  58. package/lib/queries.ts +45 -0
  59. package/lib/report.ts +156 -0
  60. package/lib/session-detail.ts +382 -0
  61. package/lib/sync.ts +87 -0
  62. package/lib/tool-flow.ts +71 -0
  63. package/next.config.mjs +6 -1
  64. package/package.json +17 -2
  65. package/plugins/cost-heatmap/component.tsx +72 -50
  66. package/prisma/migrations/20260401144555_add_system_prompt_edits/migration.sql +80 -0
  67. package/prisma/schema.prisma +18 -0
  68. package/prisma/schema.sql +81 -0
  69. package/.claude/settings.local.json +0 -26
  70. package/CONTRIBUTING.md +0 -209
  71. package/prisma/migrations/20260328152517_init/migration.sql +0 -41
  72. package/prisma/migrations/20260328153801_add_image_model/migration.sql +0 -18
  73. package/prisma.config.ts +0 -14
  74. package/setup.sh +0 -73
@@ -23,53 +23,131 @@ import {
23
23
  Cpu,
24
24
  Sun,
25
25
  CalendarClock,
26
+ AlertTriangle,
27
+ Ban,
28
+ Flame,
29
+ Zap,
30
+ Calendar,
26
31
  HelpCircle,
27
32
  } from 'lucide-react'
28
33
 
29
- function computePeakHour(sessions: SessionSummary[]): string {
34
+ function computeTopHours(sessions: SessionSummary[]): string {
30
35
  if (sessions.length === 0) return 'N/A'
31
36
  const hourCounts = new Array(24).fill(0)
32
37
  for (const s of sessions) {
33
- if (s.startTime) hourCounts[new Date(s.startTime).getHours()]++
38
+ const timestamps = s.messageTimestamps
39
+ if (timestamps && timestamps.length > 0) {
40
+ for (const ts of timestamps) {
41
+ hourCounts[new Date(ts).getHours()]++
42
+ }
43
+ } else if (s.startTime) {
44
+ hourCounts[new Date(s.startTime).getHours()]++
45
+ }
34
46
  }
35
- const peak = hourCounts.indexOf(Math.max(...hourCounts))
36
- const end = (peak + 1) % 24
37
47
  const fmt = (h: number) => `${h % 12 || 12}${h < 12 ? 'am' : 'pm'}`
38
- return `${fmt(peak)}–${fmt(end)}`
48
+ const top2 = hourCounts
49
+ .map((count, hour) => ({ hour, count }))
50
+ .sort((a, b) => b.count - a.count)
51
+ .slice(0, 2)
52
+ return top2.map((t) => fmt(t.hour)).join(', ')
39
53
  }
40
54
 
55
+ const IDLE_THRESHOLD_MS = 30 * 60 * 1000 // 30 min — gaps longer than this are idle time
56
+
41
57
  function computeAvgDailyTime(sessions: SessionSummary[]): number {
42
58
  if (sessions.length === 0) return 0
43
- // Merge overlapping sessions per day to avoid double-counting
44
- const dayIntervals = new Map<string, [number, number][]>()
59
+ // Collect all message timestamps grouped by local date.
60
+ // Sum only gaps between consecutive messages that are under the idle threshold.
61
+ const dayTimestamps = new Map<string, number[]>()
62
+
45
63
  for (const s of sessions) {
46
- if (!s.startTime || !s.endTime) continue
47
- const date = s.startTime.slice(0, 10)
48
- const start = new Date(s.startTime).getTime()
49
- const end = new Date(s.endTime).getTime()
50
- if (end <= start) continue
51
- if (!dayIntervals.has(date)) dayIntervals.set(date, [])
52
- dayIntervals.get(date)!.push([start, end])
64
+ const timestamps = s.messageTimestamps
65
+ if (!timestamps || timestamps.length === 0) continue
66
+ for (const ts of timestamps) {
67
+ const d = new Date(ts)
68
+ const ms = d.getTime()
69
+ const date = d.toLocaleDateString('en-CA')
70
+ if (!dayTimestamps.has(date)) dayTimestamps.set(date, [])
71
+ dayTimestamps.get(date)!.push(ms)
72
+ }
53
73
  }
54
74
 
55
75
  let totalMinutes = 0
56
- for (const intervals of dayIntervals.values()) {
57
- intervals.sort((a, b) => a[0] - b[0])
58
- const merged: [number, number][] = [intervals[0]]
59
- for (let i = 1; i < intervals.length; i++) {
60
- const last = merged[merged.length - 1]
61
- if (intervals[i][0] <= last[1]) {
62
- last[1] = Math.max(last[1], intervals[i][1])
63
- } else {
64
- merged.push(intervals[i])
76
+ for (const timestamps of dayTimestamps.values()) {
77
+ timestamps.sort((a, b) => a - b)
78
+ for (let i = 1; i < timestamps.length; i++) {
79
+ const gap = timestamps[i] - timestamps[i - 1]
80
+ if (gap <= IDLE_THRESHOLD_MS) {
81
+ totalMinutes += gap / 60000
65
82
  }
66
83
  }
67
- for (const [start, end] of merged) {
68
- totalMinutes += (end - start) / 60000
84
+ }
85
+
86
+ return dayTimestamps.size > 0 ? totalMinutes / dayTimestamps.size : 0
87
+ }
88
+
89
+ function computeAvgParallelSessions(sessions: SessionSummary[]): string {
90
+ if (sessions.length === 0) return '0'
91
+ // Group sessions by local date, count how many are active per day
92
+ const daySessions = new Map<string, number>()
93
+ for (const s of sessions) {
94
+ if (!s.startTime) continue
95
+ const date = new Date(s.startTime).toLocaleDateString('en-CA')
96
+ daySessions.set(date, (daySessions.get(date) || 0) + 1)
97
+ }
98
+ if (daySessions.size === 0) return '0'
99
+ const total = Array.from(daySessions.values()).reduce((a, b) => a + b, 0)
100
+ const avg = total / daySessions.size
101
+ return avg.toFixed(1)
102
+ }
103
+
104
+ function computeStreaks(sessions: SessionSummary[]): { longest: number; current: number; activeDays: number } {
105
+ if (sessions.length === 0) return { longest: 0, current: 0, activeDays: 0 }
106
+
107
+ const activeDates = new Set<string>()
108
+ for (const s of sessions) {
109
+ if (s.startTime) {
110
+ activeDates.add(new Date(s.startTime).toLocaleDateString('en-CA'))
111
+ }
112
+ }
113
+
114
+ if (activeDates.size === 0) return { longest: 0, current: 0, activeDays: 0 }
115
+
116
+ const sorted = Array.from(activeDates).sort()
117
+ let longest = 1
118
+ let streak = 1
119
+
120
+ for (let i = 1; i < sorted.length; i++) {
121
+ const prev = new Date(sorted[i - 1])
122
+ const curr = new Date(sorted[i])
123
+ const diffDays = (curr.getTime() - prev.getTime()) / (1000 * 60 * 60 * 24)
124
+ if (diffDays === 1) {
125
+ streak++
126
+ if (streak > longest) longest = streak
127
+ } else {
128
+ streak = 1
129
+ }
130
+ }
131
+
132
+ // Current streak: count backwards from today
133
+ const today = new Date().toLocaleDateString('en-CA')
134
+ const yesterday = new Date(Date.now() - 86400000).toLocaleDateString('en-CA')
135
+ let current = 0
136
+ if (activeDates.has(today) || activeDates.has(yesterday)) {
137
+ const start = activeDates.has(today) ? today : yesterday
138
+ current = 1
139
+ let d = new Date(start)
140
+ while (true) {
141
+ d = new Date(d.getTime() - 86400000)
142
+ if (activeDates.has(d.toLocaleDateString('en-CA'))) {
143
+ current++
144
+ } else {
145
+ break
146
+ }
69
147
  }
70
148
  }
71
149
 
72
- return dayIntervals.size > 0 ? totalMinutes / dayIntervals.size : 0
150
+ return { longest, current, activeDays: activeDates.size }
73
151
  }
74
152
 
75
153
  interface MetricDef {
@@ -80,8 +158,9 @@ interface MetricDef {
80
158
  }
81
159
 
82
160
  export function OverviewCards({ overview, sessions }: { overview: OverviewStats; sessions: SessionSummary[] }) {
83
- const peakHour = useMemo(() => computePeakHour(sessions), [sessions])
161
+ const topHours = useMemo(() => computeTopHours(sessions), [sessions])
84
162
  const avgDailyTime = useMemo(() => computeAvgDailyTime(sessions), [sessions])
163
+ const streaks = useMemo(() => computeStreaks(sessions), [sessions])
85
164
 
86
165
  const cards: MetricDef[] = [
87
166
  {
@@ -121,22 +200,22 @@ export function OverviewCards({ overview, sessions }: { overview: OverviewStats;
121
200
  explanation: 'Total number of tool invocations by the assistant (Read, Edit, Bash, Grep, Write, Agent, etc.). Each tool_use content block in an assistant message counts as one call.',
122
201
  },
123
202
  {
124
- title: 'Avg Daily Time',
203
+ title: 'Est. Daily Time',
125
204
  value: formatDuration(avgDailyTime),
126
205
  icon: CalendarClock,
127
- explanation: 'Average active time per day. Calculated by summing session durations for each day (merging overlapping sessions to avoid double-counting), then dividing by the number of active days. Session duration = time from first message to last message.',
206
+ explanation: 'Average active coding time per day. Computed from individual message timestamps: sums gaps between consecutive messages that are under 30 minutes (idle gaps are excluded). Then divides by the number of active days.',
128
207
  },
129
208
  {
130
- title: 'Peak Hour',
131
- value: peakHour,
209
+ title: 'Top Hours',
210
+ value: topHours,
132
211
  icon: Sun,
133
- explanation: 'The hour of day when you start the most sessions. Based on the start timestamp of each session, grouped by hour in your local timezone.',
212
+ explanation: 'The two hours of day with the most message activity. Based on individual message timestamps across all sessions, grouped by hour in your local timezone. Note: logs only store UTC timestamps, so this conversion uses your browser\'s current timezone — it is only accurate if you mostly code from one timezone.',
134
213
  },
135
214
  {
136
- title: 'Total Time',
137
- value: formatDuration(overview.totalDurationMinutes),
215
+ title: 'Avg Parallel',
216
+ value: computeAvgParallelSessions(sessions),
138
217
  icon: Clock,
139
- explanation: 'Sum of all session durations (wall-clock time from first to last message in each session). Note: overlapping sessions are counted separately, so this may exceed calendar time.',
218
+ explanation: 'Average number of sessions active in parallel per day. For each active day, counts how many sessions have overlapping time ranges, then averages across all days.',
140
219
  },
141
220
  {
142
221
  title: 'Top Model',
@@ -144,6 +223,36 @@ export function OverviewCards({ overview, sessions }: { overview: OverviewStats;
144
223
  icon: Cpu,
145
224
  explanation: 'The Claude model used in the most sessions. Determined by the model field in assistant responses. If a session uses multiple models, the last one seen is recorded.',
146
225
  },
226
+ {
227
+ title: 'Longest Streak',
228
+ value: `${streaks.longest}d`,
229
+ icon: Flame,
230
+ explanation: 'The longest consecutive-day coding streak. A day counts as active if at least one session was started that day. Helps you see your most sustained periods of AI-assisted coding.',
231
+ },
232
+ {
233
+ title: 'Current Streak',
234
+ value: `${streaks.current}d`,
235
+ icon: Zap,
236
+ explanation: 'Your current consecutive-day coding streak, counting back from today (or yesterday if you haven\'t coded today yet). Keep it going!',
237
+ },
238
+ {
239
+ title: 'Active Days',
240
+ value: formatNumber(streaks.activeDays),
241
+ icon: Calendar,
242
+ explanation: 'Total number of unique days with at least one coding session. Shows how consistently you\'ve been using AI coding tools over time.',
243
+ },
244
+ {
245
+ title: 'Rate Limit Days',
246
+ value: formatNumber(overview.totalRateLimitDays),
247
+ icon: AlertTriangle,
248
+ explanation: 'Number of unique days where you hit API rate limits. Rate limits (HTTP 429/529, "Rate limit reached/exceeded") indicate heavy token usage that day. This is a better signal of usage intensity than raw API error counts, which include unrelated server errors.',
249
+ },
250
+ {
251
+ title: 'Interruptions',
252
+ value: formatNumber(overview.totalUserInterruptions),
253
+ icon: Ban,
254
+ explanation: 'Number of times you interrupted or rejected a tool execution. Detected from tool_result messages containing "The user doesn\'t want to proceed". High counts may indicate the agent is taking unwanted actions.',
255
+ },
147
256
  ]
148
257
 
149
258
  return (
@@ -161,14 +270,12 @@ function MetricCard({ metric }: { metric: MetricDef }) {
161
270
  return (
162
271
  <Card className="group relative">
163
272
  <CardHeader className="flex flex-row items-center justify-between pb-2">
164
- <CardTitle className="text-sm font-medium text-muted-foreground">
273
+ <CardTitle className="flex items-center gap-1 text-sm font-medium text-muted-foreground">
165
274
  {metric.title}
166
- </CardTitle>
167
- <div className="flex items-center gap-1">
168
275
  <Dialog open={open} onOpenChange={setOpen}>
169
276
  <DialogTrigger
170
277
  render={<button />}
171
- className="opacity-0 group-hover:opacity-100 transition-opacity text-muted-foreground hover:text-foreground"
278
+ className="text-muted-foreground/50 hover:text-foreground transition-colors"
172
279
  >
173
280
  <HelpCircle className="h-3.5 w-3.5" />
174
281
  </DialogTrigger>
@@ -187,8 +294,8 @@ function MetricCard({ metric }: { metric: MetricDef }) {
187
294
  </div>
188
295
  </DialogContent>
189
296
  </Dialog>
190
- <metric.icon className="h-4 w-4 text-muted-foreground" />
191
- </div>
297
+ </CardTitle>
298
+ <metric.icon className="h-4 w-4 text-muted-foreground" />
192
299
  </CardHeader>
193
300
  <CardContent>
194
301
  <div className="text-2xl font-bold tracking-tight">{metric.value}</div>
@@ -0,0 +1,307 @@
1
+ 'use client'
2
+
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
4
+ import { Badge } from '@/components/ui/badge'
5
+ import {
6
+ Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
7
+ } from '@/components/ui/table'
8
+ import {
9
+ Trophy, AlertTriangle, Lightbulb, Brain, Flame, Wrench, Clock,
10
+ MessageSquare, FolderOpen, LayoutList, DollarSign, Terminal,
11
+ Zap, Search, Ban, Calendar,
12
+ } from 'lucide-react'
13
+ import { formatCost, formatDuration, formatNumber, formatTokens } from '@/lib/format'
14
+ import type { ReportContent } from '@/lib/report'
15
+ import type { CoachInsight, InsightSeverity, InsightCategory } from '@/lib/coach'
16
+
17
+ // ─── Score Ring ──────────────────────────────────────────────────────
18
+
19
+ function ScoreRing({ score, size = 140 }: { score: number; size?: number }) {
20
+ const radius = (size - 14) / 2
21
+ const circumference = 2 * Math.PI * radius
22
+ const offset = circumference - (score / 100) * circumference
23
+ const getColor = (s: number) => {
24
+ if (s >= 85) return 'var(--chart-2)'
25
+ if (s >= 70) return 'var(--chart-1)'
26
+ if (s >= 55) return 'var(--chart-3)'
27
+ return 'var(--chart-5)'
28
+ }
29
+ return (
30
+ <svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
31
+ <circle cx={size / 2} cy={size / 2} r={radius} fill="none" stroke="var(--muted)" strokeWidth="9" />
32
+ <circle cx={size / 2} cy={size / 2} r={radius} fill="none" stroke={getColor(score)} strokeWidth="9"
33
+ strokeLinecap="round" strokeDasharray={circumference} strokeDashoffset={offset}
34
+ transform={`rotate(-90 ${size / 2} ${size / 2})`} />
35
+ <text x={size / 2} y={size / 2 - 4} textAnchor="middle" dominantBaseline="middle" className="fill-foreground font-bold" style={{ fontSize: size * 0.22 }}>{score}</text>
36
+ <text x={size / 2} y={size / 2 + 16} textAnchor="middle" className="fill-muted-foreground" style={{ fontSize: size * 0.09 }}>/ 100</text>
37
+ </svg>
38
+ )
39
+ }
40
+
41
+ // ─── Insight Card ────────────────────────────────────────────────────
42
+
43
+ const SEVERITY_BORDER: Record<InsightSeverity, string> = {
44
+ achievement: 'border-l-chart-2',
45
+ warning: 'border-l-chart-5',
46
+ tip: 'border-l-chart-1',
47
+ }
48
+
49
+ const SEVERITY_ICON: Record<InsightSeverity, typeof Trophy> = {
50
+ achievement: Trophy,
51
+ warning: AlertTriangle,
52
+ tip: Lightbulb,
53
+ }
54
+
55
+ function InsightCard({ insight }: { insight: CoachInsight }) {
56
+ const Icon = SEVERITY_ICON[insight.severity]
57
+ return (
58
+ <div className={`rounded-lg border border-l-4 ${SEVERITY_BORDER[insight.severity]} p-4`}>
59
+ <div className="flex items-start gap-3">
60
+ <Icon className="h-4 w-4 shrink-0 mt-0.5 text-muted-foreground" />
61
+ <div className="space-y-1">
62
+ <div className="text-sm font-semibold">{insight.title}</div>
63
+ <p className="text-xs text-muted-foreground">{insight.description}</p>
64
+ {insight.recommendation && (
65
+ <div className="rounded-md bg-muted/50 p-2 text-xs mt-2">{insight.recommendation}</div>
66
+ )}
67
+ </div>
68
+ {insight.metric && (
69
+ <span className="shrink-0 rounded-md bg-muted px-2 py-0.5 text-xs font-mono">{insight.metric}</span>
70
+ )}
71
+ </div>
72
+ </div>
73
+ )
74
+ }
75
+
76
+ // ─── Main Report View ────────────────────────────────────────────────
77
+
78
+ export function ReportView({ content }: { content: ReportContent }) {
79
+ const g = content.atAGlance
80
+
81
+ return (
82
+ <div className="space-y-6 max-w-4xl">
83
+ {/* At a Glance */}
84
+ <Card>
85
+ <CardHeader>
86
+ <CardTitle>At a Glance</CardTitle>
87
+ <CardDescription>{g.dateRange.from} to {g.dateRange.to}</CardDescription>
88
+ </CardHeader>
89
+ <CardContent>
90
+ <div className="flex items-center gap-8">
91
+ <ScoreRing score={g.fitnessScore} />
92
+ <div className="flex-1">
93
+ <div className="text-lg font-semibold mb-1">Fitness: {g.scoreLabel}</div>
94
+ <div className="flex items-center gap-2 text-sm text-muted-foreground mb-3">
95
+ <Flame className="h-3.5 w-3.5" />
96
+ {g.currentStreak > 0 ? `${g.currentStreak}d streak` : 'No active streak'}
97
+ {g.longestStreak > 0 && <span>(best: {g.longestStreak}d)</span>}
98
+ </div>
99
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-4 text-sm">
100
+ <div><span className="text-muted-foreground">Sessions</span><div className="font-semibold">{formatNumber(g.totalSessions)}</div></div>
101
+ <div><span className="text-muted-foreground">Projects</span><div className="font-semibold">{formatNumber(g.totalProjects)}</div></div>
102
+ <div><span className="text-muted-foreground">Messages</span><div className="font-semibold">{formatNumber(g.totalMessages)}</div></div>
103
+ <div><span className="text-muted-foreground">Total Time</span><div className="font-semibold">{formatDuration(g.totalDurationMinutes)}</div></div>
104
+ <div><span className="text-muted-foreground">Tool Calls</span><div className="font-semibold">{formatNumber(g.totalToolCalls)}</div></div>
105
+ <div><span className="text-muted-foreground">Total Cost</span><div className="font-semibold">{formatCost(g.totalCostUSD)}</div></div>
106
+ <div><span className="text-muted-foreground">API Errors</span><div className="font-semibold">{formatNumber(g.totalApiErrors)}</div></div>
107
+ <div><span className="text-muted-foreground">Interruptions</span><div className="font-semibold">{formatNumber(g.totalUserInterruptions)}</div></div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </CardContent>
112
+ </Card>
113
+
114
+ {/* Project Areas */}
115
+ {content.projectAreas.length > 0 && (
116
+ <Card>
117
+ <CardHeader>
118
+ <div className="flex items-center gap-2">
119
+ <FolderOpen className="h-5 w-5 text-muted-foreground" />
120
+ <div>
121
+ <CardTitle>What You Work On</CardTitle>
122
+ <CardDescription>Top projects by session count</CardDescription>
123
+ </div>
124
+ </div>
125
+ </CardHeader>
126
+ <CardContent>
127
+ <Table>
128
+ <TableHeader>
129
+ <TableRow>
130
+ <TableHead>Project</TableHead>
131
+ <TableHead className="text-right">Sessions</TableHead>
132
+ <TableHead className="text-right">Messages</TableHead>
133
+ <TableHead className="text-right">Duration</TableHead>
134
+ <TableHead>Top Tools</TableHead>
135
+ </TableRow>
136
+ </TableHeader>
137
+ <TableBody>
138
+ {content.projectAreas.map(p => (
139
+ <TableRow key={p.name}>
140
+ <TableCell className="font-medium">{p.name}</TableCell>
141
+ <TableCell className="text-right">{p.sessions}</TableCell>
142
+ <TableCell className="text-right">{formatNumber(p.totalMessages)}</TableCell>
143
+ <TableCell className="text-right">{formatDuration(p.totalDurationMinutes)}</TableCell>
144
+ <TableCell className="text-xs text-muted-foreground">
145
+ {p.topTools.map(([name, count]) => `${name} (${count})`).join(', ')}
146
+ </TableCell>
147
+ </TableRow>
148
+ ))}
149
+ </TableBody>
150
+ </Table>
151
+ </CardContent>
152
+ </Card>
153
+ )}
154
+
155
+ {/* Interaction Style */}
156
+ <Card>
157
+ <CardHeader>
158
+ <div className="flex items-center gap-2">
159
+ <Brain className="h-5 w-5 text-muted-foreground" />
160
+ <div>
161
+ <CardTitle>How You Use Your Agent</CardTitle>
162
+ <CardDescription>Interaction patterns and coding style</CardDescription>
163
+ </div>
164
+ </div>
165
+ </CardHeader>
166
+ <CardContent>
167
+ <div className="flex items-start gap-6 mb-4">
168
+ <div>
169
+ <div className="text-3xl font-bold tracking-wider text-primary">{content.interactionStyle.mbtiType}</div>
170
+ <div className="text-xs text-muted-foreground mt-1">{content.interactionStyle.mbtiDescription}</div>
171
+ </div>
172
+ </div>
173
+ <div className="grid grid-cols-2 gap-4 sm:grid-cols-4 text-sm">
174
+ <div>
175
+ <span className="text-muted-foreground">Avg Messages/Session</span>
176
+ <div className="font-semibold">{Math.round(content.interactionStyle.avgMessagesPerSession)}</div>
177
+ </div>
178
+ <div>
179
+ <span className="text-muted-foreground">Avg Duration</span>
180
+ <div className="font-semibold">{formatDuration(content.interactionStyle.avgDurationMinutes)}</div>
181
+ </div>
182
+ <div>
183
+ <span className="text-muted-foreground">Read/Edit Ratio</span>
184
+ <div className="font-semibold">{content.interactionStyle.readEditRatio.toFixed(1)}x</div>
185
+ </div>
186
+ <div>
187
+ <span className="text-muted-foreground">Bash Usage</span>
188
+ <div className="font-semibold">{Math.round(content.interactionStyle.bashRatio * 100)}%</div>
189
+ </div>
190
+ <div>
191
+ <span className="text-muted-foreground">Subagent Calls</span>
192
+ <div className="font-semibold">{formatNumber(content.interactionStyle.agentCallsTotal)}</div>
193
+ </div>
194
+ <div>
195
+ <span className="text-muted-foreground">Peak Hour</span>
196
+ <div className="font-semibold">{content.interactionStyle.peakHour}:00</div>
197
+ </div>
198
+ <div>
199
+ <span className="text-muted-foreground">Most Active Day</span>
200
+ <div className="font-semibold">{content.interactionStyle.mostActiveDay}</div>
201
+ </div>
202
+ <div>
203
+ <span className="text-muted-foreground">Avg Cost/Session</span>
204
+ <div className="font-semibold">{formatCost(content.interactionStyle.avgCostPerSession)}</div>
205
+ </div>
206
+ </div>
207
+ </CardContent>
208
+ </Card>
209
+
210
+ {/* What Works */}
211
+ {content.whatWorks.length > 0 && (
212
+ <Card>
213
+ <CardHeader>
214
+ <div className="flex items-center gap-2">
215
+ <Trophy className="h-5 w-5 text-chart-2" />
216
+ <div>
217
+ <CardTitle>What's Working</CardTitle>
218
+ <CardDescription>Patterns and habits that are paying off</CardDescription>
219
+ </div>
220
+ </div>
221
+ </CardHeader>
222
+ <CardContent className="space-y-3">
223
+ {content.whatWorks.map(i => <InsightCard key={i.id} insight={i} />)}
224
+ </CardContent>
225
+ </Card>
226
+ )}
227
+
228
+ {/* Friction Analysis */}
229
+ {content.frictionAnalysis.length > 0 && (
230
+ <Card>
231
+ <CardHeader>
232
+ <div className="flex items-center gap-2">
233
+ <AlertTriangle className="h-5 w-5 text-chart-5" />
234
+ <div>
235
+ <CardTitle>Where Things Go Wrong</CardTitle>
236
+ <CardDescription>Friction points that slow you down</CardDescription>
237
+ </div>
238
+ </div>
239
+ </CardHeader>
240
+ <CardContent className="space-y-3">
241
+ {content.frictionAnalysis.map(i => <InsightCard key={i.id} insight={i} />)}
242
+ </CardContent>
243
+ </Card>
244
+ )}
245
+
246
+ {/* Suggestions */}
247
+ <Card>
248
+ <CardHeader>
249
+ <div className="flex items-center gap-2">
250
+ <Lightbulb className="h-5 w-5 text-chart-1" />
251
+ <div>
252
+ <CardTitle>Suggestions</CardTitle>
253
+ <CardDescription>Actionable ways to improve your workflow</CardDescription>
254
+ </div>
255
+ </div>
256
+ </CardHeader>
257
+ <CardContent className="space-y-6">
258
+ {/* CLAUDE.md rules */}
259
+ {content.suggestions.claudeMdRules.length > 0 && (
260
+ <div>
261
+ <h4 className="text-sm font-semibold mb-2 flex items-center gap-1.5">
262
+ <Terminal className="h-4 w-4" /> Add to CLAUDE.md
263
+ </h4>
264
+ <div className="space-y-2">
265
+ {content.suggestions.claudeMdRules.map((rule, i) => (
266
+ <div key={i} className="rounded-md bg-muted p-3 text-xs font-mono whitespace-pre-wrap">{rule}</div>
267
+ ))}
268
+ </div>
269
+ </div>
270
+ )}
271
+
272
+ {/* Coach tips */}
273
+ {content.suggestions.coachTips.length > 0 && (
274
+ <div>
275
+ <h4 className="text-sm font-semibold mb-2 flex items-center gap-1.5">
276
+ <Zap className="h-4 w-4" /> Workflow Tips
277
+ </h4>
278
+ <div className="space-y-3">
279
+ {content.suggestions.coachTips.map(i => <InsightCard key={i.id} insight={i} />)}
280
+ </div>
281
+ </div>
282
+ )}
283
+
284
+ {/* Command tips */}
285
+ {content.suggestions.commandTips.length > 0 && (
286
+ <div>
287
+ <h4 className="text-sm font-semibold mb-2 flex items-center gap-1.5">
288
+ <Search className="h-4 w-4" /> Commands to Discover
289
+ </h4>
290
+ <div className="space-y-2">
291
+ {content.suggestions.commandTips.map(i => (
292
+ <div key={i.id} className="rounded-lg border p-3">
293
+ <div className="text-sm font-semibold">{i.title}</div>
294
+ <p className="text-xs text-muted-foreground mt-0.5">{i.description}</p>
295
+ {i.recommendation && (
296
+ <div className="rounded-md bg-muted/50 p-2 text-xs mt-2">{i.recommendation}</div>
297
+ )}
298
+ </div>
299
+ ))}
300
+ </div>
301
+ </div>
302
+ )}
303
+ </CardContent>
304
+ </Card>
305
+ </div>
306
+ )
307
+ }