prjct-cli 0.10.14 → 0.11.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 (105) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/bin/dev.js +217 -0
  3. package/bin/prjct +10 -0
  4. package/bin/serve.js +78 -0
  5. package/core/bus/index.js +322 -0
  6. package/core/command-registry.js +65 -0
  7. package/core/domain/snapshot-manager.js +375 -0
  8. package/core/plugin/hooks.js +313 -0
  9. package/core/plugin/index.js +52 -0
  10. package/core/plugin/loader.js +331 -0
  11. package/core/plugin/registry.js +325 -0
  12. package/core/plugins/webhook.js +143 -0
  13. package/core/session/index.js +449 -0
  14. package/core/session/metrics.js +293 -0
  15. package/package.json +28 -4
  16. package/packages/shared/dist/index.d.ts +615 -0
  17. package/packages/shared/dist/index.js +204 -0
  18. package/packages/shared/package.json +29 -0
  19. package/packages/shared/src/index.ts +9 -0
  20. package/packages/shared/src/schemas.ts +124 -0
  21. package/packages/shared/src/types.ts +187 -0
  22. package/packages/shared/src/utils.ts +148 -0
  23. package/packages/shared/tsconfig.json +18 -0
  24. package/packages/web/README.md +36 -0
  25. package/packages/web/app/api/claude/sessions/route.ts +44 -0
  26. package/packages/web/app/api/claude/status/route.ts +34 -0
  27. package/packages/web/app/api/projects/[id]/delete/route.ts +21 -0
  28. package/packages/web/app/api/projects/[id]/icon/route.ts +33 -0
  29. package/packages/web/app/api/projects/[id]/route.ts +29 -0
  30. package/packages/web/app/api/projects/[id]/stats/route.ts +36 -0
  31. package/packages/web/app/api/projects/[id]/status/route.ts +21 -0
  32. package/packages/web/app/api/projects/route.ts +16 -0
  33. package/packages/web/app/api/sessions/history/route.ts +122 -0
  34. package/packages/web/app/api/stats/route.ts +38 -0
  35. package/packages/web/app/error.tsx +34 -0
  36. package/packages/web/app/favicon.ico +0 -0
  37. package/packages/web/app/globals.css +155 -0
  38. package/packages/web/app/layout.tsx +43 -0
  39. package/packages/web/app/loading.tsx +7 -0
  40. package/packages/web/app/not-found.tsx +25 -0
  41. package/packages/web/app/page.tsx +227 -0
  42. package/packages/web/app/project/[id]/error.tsx +41 -0
  43. package/packages/web/app/project/[id]/loading.tsx +9 -0
  44. package/packages/web/app/project/[id]/not-found.tsx +27 -0
  45. package/packages/web/app/project/[id]/page.tsx +253 -0
  46. package/packages/web/app/project/[id]/stats/page.tsx +447 -0
  47. package/packages/web/app/sessions/page.tsx +165 -0
  48. package/packages/web/app/settings/page.tsx +150 -0
  49. package/packages/web/components/AppSidebar.tsx +113 -0
  50. package/packages/web/components/CommandButton.tsx +39 -0
  51. package/packages/web/components/ConnectionStatus.tsx +29 -0
  52. package/packages/web/components/Logo.tsx +65 -0
  53. package/packages/web/components/MarkdownContent.tsx +123 -0
  54. package/packages/web/components/ProjectAvatar.tsx +54 -0
  55. package/packages/web/components/TechStackBadges.tsx +20 -0
  56. package/packages/web/components/TerminalTab.tsx +84 -0
  57. package/packages/web/components/TerminalTabs.tsx +210 -0
  58. package/packages/web/components/charts/SessionsChart.tsx +172 -0
  59. package/packages/web/components/providers.tsx +45 -0
  60. package/packages/web/components/ui/alert-dialog.tsx +157 -0
  61. package/packages/web/components/ui/badge.tsx +46 -0
  62. package/packages/web/components/ui/button.tsx +60 -0
  63. package/packages/web/components/ui/card.tsx +92 -0
  64. package/packages/web/components/ui/chart.tsx +385 -0
  65. package/packages/web/components/ui/dropdown-menu.tsx +257 -0
  66. package/packages/web/components/ui/scroll-area.tsx +58 -0
  67. package/packages/web/components/ui/sheet.tsx +139 -0
  68. package/packages/web/components/ui/tabs.tsx +66 -0
  69. package/packages/web/components/ui/tooltip.tsx +61 -0
  70. package/packages/web/components.json +22 -0
  71. package/packages/web/context/TerminalContext.tsx +45 -0
  72. package/packages/web/context/TerminalTabsContext.tsx +136 -0
  73. package/packages/web/eslint.config.mjs +18 -0
  74. package/packages/web/hooks/useClaudeTerminal.ts +375 -0
  75. package/packages/web/hooks/useProjectStats.ts +38 -0
  76. package/packages/web/hooks/useProjects.ts +73 -0
  77. package/packages/web/hooks/useStats.ts +28 -0
  78. package/packages/web/lib/format.ts +23 -0
  79. package/packages/web/lib/parse-prjct-files.ts +1122 -0
  80. package/packages/web/lib/projects.ts +452 -0
  81. package/packages/web/lib/pty.ts +101 -0
  82. package/packages/web/lib/query-config.ts +44 -0
  83. package/packages/web/lib/utils.ts +6 -0
  84. package/packages/web/next-env.d.ts +6 -0
  85. package/packages/web/next.config.ts +7 -0
  86. package/packages/web/package.json +53 -0
  87. package/packages/web/postcss.config.mjs +7 -0
  88. package/packages/web/public/file.svg +1 -0
  89. package/packages/web/public/globe.svg +1 -0
  90. package/packages/web/public/next.svg +1 -0
  91. package/packages/web/public/vercel.svg +1 -0
  92. package/packages/web/public/window.svg +1 -0
  93. package/packages/web/server.ts +262 -0
  94. package/packages/web/tsconfig.json +34 -0
  95. package/templates/commands/done.md +176 -54
  96. package/templates/commands/history.md +176 -0
  97. package/templates/commands/init.md +28 -1
  98. package/templates/commands/now.md +191 -9
  99. package/templates/commands/pause.md +176 -12
  100. package/templates/commands/redo.md +142 -0
  101. package/templates/commands/resume.md +166 -62
  102. package/templates/commands/serve.md +121 -0
  103. package/templates/commands/ship.md +45 -1
  104. package/templates/commands/sync.md +34 -1
  105. package/templates/commands/undo.md +152 -0
@@ -0,0 +1,447 @@
1
+ 'use client'
2
+
3
+ import { use, useMemo, useState } from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+ import Link from 'next/link'
6
+ import { useProject } from '@/hooks/useProjects'
7
+ import { useProjectStats } from '@/hooks/useProjectStats'
8
+ import { Button } from '@/components/ui/button'
9
+ import {
10
+ ArrowLeft,
11
+ Zap,
12
+ TrendingUp,
13
+ TrendingDown,
14
+ Lightbulb,
15
+ AlertTriangle,
16
+ CheckCircle2,
17
+ Bot,
18
+ Rocket,
19
+ Flame,
20
+ Play,
21
+ Copy,
22
+ FileText,
23
+ Target,
24
+ Clock
25
+ } from 'lucide-react'
26
+ import { cn } from '@/lib/utils'
27
+ import type { TimelineEvent } from '@/lib/parse-prjct-files'
28
+
29
+ // Calculate streak
30
+ function calculateStreak(timeline: TimelineEvent[]): number {
31
+ if (!timeline.length) return 0
32
+ const dates = new Set(timeline.map(e => e.ts?.split('T')[0]).filter(Boolean))
33
+ let streak = 0
34
+ const today = new Date()
35
+ for (let i = 0; i < 30; i++) {
36
+ const date = new Date(today)
37
+ date.setDate(date.getDate() - i)
38
+ const dateStr = date.toISOString().split('T')[0]
39
+ if (dates.has(dateStr)) streak++
40
+ else if (i > 0) break
41
+ }
42
+ return streak
43
+ }
44
+
45
+ // Health score (0-100)
46
+ function getHealthScore(stats: any): number {
47
+ if (!stats) return 0
48
+ const velocity = stats?.metrics?.velocity?.tasksPerDay || 0
49
+ const hasCurrentTask = !!stats?.currentTask
50
+ const queueSize = stats?.queue?.length || 0
51
+ const recentActivity = stats?.timeline?.slice(0, 7).length || 0
52
+
53
+ let score = 0
54
+ score += Math.min(30, velocity * 15) // Up to 30 for velocity
55
+ score += hasCurrentTask ? 20 : 0 // 20 for active work
56
+ score += queueSize > 0 && queueSize < 15 ? 20 : queueSize === 0 ? 5 : 10 // Queue health
57
+ score += Math.min(30, recentActivity * 5) // Recent activity
58
+
59
+ return Math.min(100, Math.round(score))
60
+ }
61
+
62
+ // Copy to clipboard
63
+ function copyCommand(cmd: string) {
64
+ navigator.clipboard.writeText(cmd)
65
+ }
66
+
67
+ // Format relative time
68
+ function formatRelativeTime(dateString: string): string {
69
+ const date = new Date(dateString)
70
+ const now = new Date()
71
+ const diffMs = now.getTime() - date.getTime()
72
+ const diffMins = Math.floor(diffMs / 60000)
73
+ const diffHours = Math.floor(diffMs / 3600000)
74
+ const diffDays = Math.floor(diffMs / 86400000)
75
+
76
+ if (diffMins < 1) return 'NOW'
77
+ if (diffMins < 60) return `${diffMins}M`
78
+ if (diffHours < 24) return `${diffHours}H`
79
+ if (diffDays === 1) return '1D'
80
+ if (diffDays < 7) return `${diffDays}D`
81
+ return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }).toUpperCase()
82
+ }
83
+
84
+ // Contextual insight message
85
+ function getInsightMessage(stats: any, streak: number): string {
86
+ if (!stats) return ''
87
+
88
+ const velocity = stats?.metrics?.velocity?.tasksPerDay || 0
89
+ const hasCurrentTask = !!stats?.currentTask
90
+ const queueSize = stats?.queue?.length || 0
91
+ const shipsCount = stats?.summary?.totalShipsEver || 0
92
+
93
+ if (hasCurrentTask && streak > 3) return 'Killing it. Keep the momentum.'
94
+ if (hasCurrentTask) return 'Good focus. Ship when ready.'
95
+ if (queueSize === 0) return 'Queue empty. Time to plan the next feature.'
96
+ if (velocity > 2) return 'Fast pace. Watch for burnout.'
97
+ if (shipsCount === 0) return 'No ships yet. Start small, ship fast.'
98
+ if (streak === 0) return 'Get back in the flow. Start something.'
99
+ return 'Steady progress. Pick the next task.'
100
+ }
101
+
102
+ // Section label component
103
+ function SectionLabel({ children, className }: { children: React.ReactNode; className?: string }) {
104
+ return (
105
+ <p className={cn("text-[9px] font-bold uppercase tracking-[0.2em] text-foreground/40", className)}>
106
+ {children}
107
+ </p>
108
+ )
109
+ }
110
+
111
+ // Health ring component
112
+ function HealthRing({ score, size = 'md' }: { score: number; size?: 'sm' | 'md' | 'lg' }) {
113
+ const sizes = {
114
+ sm: { container: 'h-8 w-8', text: 'text-[10px]' },
115
+ md: { container: 'h-12 w-12', text: 'text-xs' },
116
+ lg: { container: 'h-16 w-16', text: 'text-sm' },
117
+ }
118
+ const { container, text } = sizes[size]
119
+
120
+ return (
121
+ <div className={cn('relative', container)}>
122
+ <svg className="h-full w-full -rotate-90" viewBox="0 0 36 36">
123
+ <circle cx="18" cy="18" r="15" fill="none" stroke="currentColor" strokeWidth="3" className="text-foreground/10" />
124
+ <circle
125
+ cx="18" cy="18" r="15" fill="none" stroke="currentColor" strokeWidth="3"
126
+ strokeDasharray={`${score} 100`}
127
+ strokeLinecap="round"
128
+ className="text-foreground transition-all duration-700"
129
+ />
130
+ </svg>
131
+ <span className={cn('absolute inset-0 flex items-center justify-center font-bold', text)}>
132
+ {score}
133
+ </span>
134
+ </div>
135
+ )
136
+ }
137
+
138
+ // Stat card component
139
+ function StatCard({ value, label, suffix, size = 'md' }: {
140
+ value: string | number
141
+ label: string
142
+ suffix?: string
143
+ size?: 'sm' | 'md' | 'lg'
144
+ }) {
145
+ const sizeClasses = {
146
+ sm: { value: 'text-lg font-bold', label: 'text-[9px] font-bold uppercase tracking-[0.2em] text-foreground/40' },
147
+ md: { value: 'text-2xl font-bold', label: 'text-xs text-muted-foreground' },
148
+ lg: { value: 'text-3xl font-black tracking-tight tabular-nums', label: 'text-[9px] font-bold uppercase tracking-[0.2em] text-foreground/40 mt-1' },
149
+ }
150
+ const styles = sizeClasses[size]
151
+
152
+ return (
153
+ <div>
154
+ <p className={cn(styles.value, 'text-foreground')}>
155
+ {value}
156
+ {suffix && <span className="text-foreground/50 font-normal text-sm ml-1">{suffix}</span>}
157
+ </p>
158
+ <p className={styles.label}>{label}</p>
159
+ </div>
160
+ )
161
+ }
162
+
163
+ // Stats row component - minimal divider
164
+ function StatsRow({ children, className }: { children: React.ReactNode; className?: string }) {
165
+ return (
166
+ <div className={cn('flex flex-wrap gap-x-10 gap-y-4', className)}>
167
+ {children}
168
+ </div>
169
+ )
170
+ }
171
+
172
+ export default function ProjectStatsPage({ params }: { params: Promise<{ id: string }> }) {
173
+ const { id: projectId } = use(params)
174
+ const router = useRouter()
175
+ const [expandedSection, setExpandedSection] = useState<string | null>(null)
176
+
177
+ const { data: project, isLoading: projectLoading } = useProject(projectId)
178
+ const { data, isLoading: statsLoading } = useProjectStats(projectId)
179
+ const stats = data?.stats
180
+
181
+ const streak = useMemo(() => calculateStreak(stats?.timeline || []), [stats?.timeline])
182
+ const healthScore = useMemo(() => getHealthScore(stats), [stats])
183
+ const insightMessage = useMemo(() => getInsightMessage(stats, streak), [stats, streak])
184
+
185
+ const completionRate = useMemo(() => {
186
+ if (!stats?.metrics) return 0
187
+ const { tasksStarted, tasksCompleted } = stats.metrics
188
+ return tasksStarted > 0 ? Math.round((tasksCompleted / tasksStarted) * 100) : 0
189
+ }, [stats?.metrics])
190
+
191
+ const velocityChange = useMemo(() => {
192
+ // Simulated - in reality would compare to previous period
193
+ const velocity = stats?.metrics?.velocity?.tasksPerDay || 0
194
+ return velocity > 2 ? 15 : velocity > 1 ? 5 : -10
195
+ }, [stats?.metrics?.velocity?.tasksPerDay])
196
+
197
+ // Recent activity - last 5 for chips
198
+ const recentActivity = useMemo(() => {
199
+ if (!stats?.timeline) return []
200
+ return stats.timeline.slice(0, 5)
201
+ }, [stats?.timeline])
202
+
203
+ if (projectLoading || statsLoading) {
204
+ return <div className="flex items-center justify-center h-full"><div className="animate-pulse text-muted-foreground">Loading...</div></div>
205
+ }
206
+
207
+ if (!project || !stats) {
208
+ return (
209
+ <div className="flex items-center justify-center h-full">
210
+ <div className="text-center space-y-4">
211
+ <p className="text-4xl text-muted-foreground">404</p>
212
+ <Button variant="ghost" onClick={() => router.back()}><ArrowLeft className="w-4 h-4 mr-2" />Back</Button>
213
+ </div>
214
+ </div>
215
+ )
216
+ }
217
+
218
+ return (
219
+ <div className="flex h-full flex-col p-8 overflow-auto">
220
+ {/* Hero Section - Big number + insight */}
221
+ <div className="flex items-start justify-between mb-2">
222
+ <div>
223
+ {/* Big Number - Tasks Completed */}
224
+ <h1 className="text-8xl font-bold tracking-tighter text-foreground tabular-nums">
225
+ {stats.metrics.tasksCompleted}
226
+ </h1>
227
+ <p className="text-lg text-muted-foreground mt-2">{insightMessage}</p>
228
+ </div>
229
+
230
+ {/* Navigation back */}
231
+ <Link href={`/project/${projectId}`} className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground">
232
+ <ArrowLeft className="w-4 h-4" />{project.name}
233
+ </Link>
234
+ </div>
235
+
236
+ {/* Trend indicator */}
237
+ {velocityChange !== 0 && (
238
+ <div className="flex items-center gap-2 mt-4">
239
+ <span className={cn(
240
+ 'flex items-center gap-1 text-sm font-medium px-2 py-1 rounded-md',
241
+ velocityChange >= 0 ? 'bg-foreground/5 text-foreground' : 'bg-muted text-muted-foreground'
242
+ )}>
243
+ {velocityChange >= 0 ? <TrendingUp className="h-3.5 w-3.5" /> : <TrendingDown className="h-3.5 w-3.5" />}
244
+ {velocityChange >= 0 ? '+' : ''}{velocityChange}%
245
+ </span>
246
+ <span className="text-sm text-muted-foreground">velocity vs last week</span>
247
+ </div>
248
+ )}
249
+
250
+ {/* Secondary KPIs Row */}
251
+ <StatsRow className="mt-8">
252
+ <div className="flex items-center gap-3">
253
+ <HealthRing score={healthScore} />
254
+ <div>
255
+ <p className="text-sm font-medium">Health</p>
256
+ <p className="text-xs text-muted-foreground">Project score</p>
257
+ </div>
258
+ </div>
259
+
260
+ <StatCard value={`${completionRate}%`} label="Completion" />
261
+ <StatCard value={stats.summary.totalShipsEver} label="Ships" />
262
+ <StatCard value={stats.metrics.velocity.tasksPerDay} label="Tasks/day" />
263
+ <StatCard
264
+ value={<>{stats.queue.length} / {stats.ideas.pending.length}</>}
265
+ label="Queue / Ideas"
266
+ />
267
+ {streak > 0 && (
268
+ <div className="flex items-center gap-2">
269
+ <Flame className={cn('w-5 h-5', streak > 3 ? 'text-orange-500' : 'text-foreground/40')} />
270
+ <StatCard value={streak} label="Day streak" />
271
+ </div>
272
+ )}
273
+ </StatsRow>
274
+
275
+ {/* Recent Activity - Chips style */}
276
+ {recentActivity.length > 0 && (
277
+ <div className="mt-8">
278
+ <SectionLabel className="mb-4">RECENT</SectionLabel>
279
+ <div className="flex flex-wrap gap-3">
280
+ {recentActivity.map((event: TimelineEvent, i: number) => {
281
+ const e = event as Record<string, unknown>
282
+ const label = event.type === 'feature_ship' ? (e.name as string) :
283
+ event.type === 'task_complete' ? (e.task as string) :
284
+ event.type === 'task_start' ? (e.task as string) :
285
+ event.type === 'sync' ? 'Sync' : event.type
286
+ const status = event.type === 'feature_ship' ? 'SHIP' :
287
+ event.type === 'task_complete' ? 'DONE' :
288
+ event.type === 'task_start' ? 'START' :
289
+ event.type === 'sync' ? 'SYNC' : event.type.toUpperCase()
290
+ return (
291
+ <div
292
+ key={i}
293
+ className="group flex items-center gap-3 rounded-lg border-2 border-foreground/10 px-4 py-2 transition-all hover:border-foreground hover:bg-foreground hover:text-background cursor-default"
294
+ >
295
+ <span className="text-sm font-bold text-foreground group-hover:text-background truncate max-w-[150px]">
296
+ {label}
297
+ </span>
298
+ <span className="text-[9px] font-bold tracking-wider text-foreground/40 group-hover:text-background/60">
299
+ {status}
300
+ </span>
301
+ {event.ts && (
302
+ <span className="text-[9px] font-bold tracking-wider text-foreground/30 group-hover:text-background/40">
303
+ {formatRelativeTime(event.ts)}
304
+ </span>
305
+ )}
306
+ </div>
307
+ )
308
+ })}
309
+ </div>
310
+ </div>
311
+ )}
312
+
313
+ {/* Masonry Grid Layout */}
314
+ <div className="columns-1 md:columns-2 lg:columns-3 gap-8 mt-10 [column-fill:_balance]">
315
+ {/* Current Task */}
316
+ <div className="break-inside-avoid mb-8">
317
+ <SectionLabel className="mb-3">NOW</SectionLabel>
318
+ {stats.currentTask ? (
319
+ <div>
320
+ <div className="flex items-center gap-2 mb-2">
321
+ <div className="w-1.5 h-1.5 rounded-full bg-amber-500 animate-pulse" />
322
+ <span className="text-[10px] font-medium text-amber-600 uppercase tracking-wider">Working</span>
323
+ </div>
324
+ <p className="text-lg font-semibold leading-tight">{stats.currentTask.task}</p>
325
+ {stats.currentTask.duration && (
326
+ <p className="text-xs text-muted-foreground mt-2">{stats.currentTask.duration}</p>
327
+ )}
328
+ </div>
329
+ ) : (
330
+ <p className="text-sm text-muted-foreground">No active task</p>
331
+ )}
332
+ </div>
333
+
334
+ {/* Queue */}
335
+ {stats.queue.length > 0 && (
336
+ <div className="break-inside-avoid mb-8">
337
+ <div className="flex items-center gap-2 mb-3">
338
+ <SectionLabel>QUEUE</SectionLabel>
339
+ <span className="text-xs text-muted-foreground">{stats.queue.length}</span>
340
+ </div>
341
+ <div className="space-y-1.5">
342
+ {stats.queue.slice(0, 6).map((task: any, i: number) => (
343
+ <div key={i} className="flex items-center gap-2 group">
344
+ <span className="text-[10px] text-muted-foreground w-3">{i + 1}</span>
345
+ <p className="flex-1 text-sm truncate">{task.task}</p>
346
+ </div>
347
+ ))}
348
+ {stats.queue.length > 6 && (
349
+ <p className="text-[10px] text-muted-foreground">+{stats.queue.length - 6} more</p>
350
+ )}
351
+ </div>
352
+ </div>
353
+ )}
354
+
355
+ {/* Ships */}
356
+ {stats.shipped.length > 0 && (
357
+ <div className="break-inside-avoid mb-8">
358
+ <SectionLabel className="mb-3">SHIPS</SectionLabel>
359
+ <div className="space-y-3">
360
+ {stats.shipped.slice(0, 5).map((ship: any, i: number) => (
361
+ <div key={i}>
362
+ <p className="text-sm font-medium">{ship.name}</p>
363
+ <p className="text-[10px] text-muted-foreground">
364
+ {new Date(ship.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
365
+ {ship.version && ` · ${ship.version}`}
366
+ </p>
367
+ </div>
368
+ ))}
369
+ </div>
370
+ </div>
371
+ )}
372
+
373
+ {/* Agents */}
374
+ {stats.agents.length > 0 && (
375
+ <div className="break-inside-avoid mb-8">
376
+ <div className="flex items-center gap-2 mb-3">
377
+ <SectionLabel>AGENTS</SectionLabel>
378
+ <span className="text-xs text-muted-foreground">{stats.agents.length}</span>
379
+ </div>
380
+ <div className="flex flex-wrap gap-1.5">
381
+ {stats.agents.slice(0, 10).map((agent: any) => (
382
+ <span
383
+ key={agent.name}
384
+ className="text-xs px-2 py-0.5 bg-foreground/5 rounded"
385
+ >
386
+ {agent.name}
387
+ </span>
388
+ ))}
389
+ </div>
390
+ </div>
391
+ )}
392
+
393
+ {/* Ideas */}
394
+ {stats.ideas.pending.length > 0 && (
395
+ <div className="break-inside-avoid mb-8">
396
+ <div className="flex items-center gap-2 mb-3">
397
+ <SectionLabel>IDEAS</SectionLabel>
398
+ <span className="text-xs text-muted-foreground">{stats.ideas.pending.length}</span>
399
+ </div>
400
+ <div className="space-y-2">
401
+ {stats.ideas.pending.slice(0, 5).map((idea: any, i: number) => (
402
+ <div key={i} className="flex items-start gap-2">
403
+ <Lightbulb className={cn(
404
+ 'w-3 h-3 mt-0.5 shrink-0',
405
+ idea.impact === 'HIGH' ? 'text-foreground' : 'text-muted-foreground'
406
+ )} />
407
+ <p className="text-sm">{idea.title}</p>
408
+ </div>
409
+ ))}
410
+ </div>
411
+ </div>
412
+ )}
413
+
414
+ {/* Roadmap */}
415
+ {stats.roadmap?.phases?.length > 0 && (
416
+ <div className="break-inside-avoid mb-8">
417
+ <div className="flex items-center justify-between mb-3">
418
+ <SectionLabel>ROADMAP</SectionLabel>
419
+ <span className="text-lg font-bold">{stats.roadmap.progress}%</span>
420
+ </div>
421
+ <div className="space-y-2">
422
+ {stats.roadmap.phases.filter((p: any) => (p.features || []).length > 0).slice(0, 4).map((phase: any) => (
423
+ <div key={phase.name}>
424
+ <div className="flex items-center justify-between text-xs mb-1">
425
+ <span>{phase.name}</span>
426
+ <span className="text-muted-foreground">{phase.progress}%</span>
427
+ </div>
428
+ <div className="h-1 bg-muted rounded-full overflow-hidden">
429
+ <div
430
+ className={cn(
431
+ 'h-full rounded-full',
432
+ phase.progress === 100 ? 'bg-emerald-500' : 'bg-foreground'
433
+ )}
434
+ style={{ width: `${phase.progress}%` }}
435
+ />
436
+ </div>
437
+ </div>
438
+ ))}
439
+ </div>
440
+ </div>
441
+ )}
442
+ </div>
443
+
444
+ <div className="h-8" />
445
+ </div>
446
+ )
447
+ }
@@ -0,0 +1,165 @@
1
+ 'use client'
2
+
3
+ import { useQuery } from '@tanstack/react-query'
4
+ import { Clock, Target, CheckCircle, PauseCircle, GitCommit, FileCode } from 'lucide-react'
5
+
6
+ function formatDuration(ms: number): string {
7
+ const hours = Math.floor(ms / 3600000)
8
+ const minutes = Math.floor((ms % 3600000) / 60000)
9
+ if (hours > 0) return `${hours}h ${minutes}m`
10
+ return `${minutes}m`
11
+ }
12
+
13
+ function getRelativeTime(dateString: string): string {
14
+ const date = new Date(dateString)
15
+ const now = new Date()
16
+ const diff = now.getTime() - date.getTime()
17
+ const minutes = Math.floor(diff / 60000)
18
+ const hours = Math.floor(diff / 3600000)
19
+ const days = Math.floor(diff / 86400000)
20
+
21
+ if (minutes < 1) return 'just now'
22
+ if (minutes < 60) return `${minutes}m ago`
23
+ if (hours < 24) return `${hours}h ago`
24
+ return `${days}d ago`
25
+ }
26
+
27
+ export default function Sessions() {
28
+ const { data: projects } = useQuery({
29
+ queryKey: ['projects'],
30
+ queryFn: async () => {
31
+ const res = await fetch('/api/projects')
32
+ const json = await res.json()
33
+ return json.data || []
34
+ }
35
+ })
36
+
37
+ // Get sessions from first project (for demo)
38
+ const projectId = projects?.[0]?.id
39
+
40
+ const { data: sessions, isLoading } = useQuery({
41
+ queryKey: ['sessions', projectId],
42
+ queryFn: async () => {
43
+ if (!projectId) return []
44
+ const res = await fetch(`/api/sessions?projectId=${projectId}`)
45
+ const json = await res.json()
46
+ return json.data || []
47
+ },
48
+ enabled: !!projectId
49
+ })
50
+
51
+ if (isLoading) {
52
+ return (
53
+ <div className="flex items-center justify-center h-full">
54
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
55
+ </div>
56
+ )
57
+ }
58
+
59
+ return (
60
+ <div className="p-6 h-full overflow-auto">
61
+ <header className="mb-6">
62
+ <h1 className="text-2xl font-bold flex items-center gap-2">
63
+ <Clock className="w-6 h-6" />
64
+ Sessions
65
+ </h1>
66
+ <p className="text-muted-foreground mt-1">
67
+ Your work session history
68
+ </p>
69
+ </header>
70
+
71
+ {!sessions?.length ? (
72
+ <div className="border border-dashed border-border rounded-lg p-8 text-center">
73
+ <Clock className="w-12 h-12 text-muted-foreground mx-auto mb-4" />
74
+ <p className="text-muted-foreground">
75
+ No sessions yet. Start one with <code className="bg-muted px-2 py-1 rounded">/p:now</code>
76
+ </p>
77
+ </div>
78
+ ) : (
79
+ <div className="space-y-4">
80
+ {sessions.map((session: Session) => (
81
+ <SessionCard key={session.id} session={session} />
82
+ ))}
83
+ </div>
84
+ )}
85
+ </div>
86
+ )
87
+ }
88
+
89
+ interface Session {
90
+ id: string
91
+ task: string
92
+ status: 'active' | 'paused' | 'completed'
93
+ startedAt: string
94
+ completedAt?: string
95
+ duration: number
96
+ metrics: {
97
+ filesChanged: number
98
+ linesAdded: number
99
+ linesRemoved: number
100
+ commits: number
101
+ }
102
+ }
103
+
104
+ function SessionCard({ session }: { session: Session }) {
105
+ const statusConfig = {
106
+ active: {
107
+ icon: Target,
108
+ color: 'text-green-500',
109
+ bg: 'bg-green-500/10',
110
+ label: 'Active'
111
+ },
112
+ paused: {
113
+ icon: PauseCircle,
114
+ color: 'text-yellow-500',
115
+ bg: 'bg-yellow-500/10',
116
+ label: 'Paused'
117
+ },
118
+ completed: {
119
+ icon: CheckCircle,
120
+ color: 'text-blue-500',
121
+ bg: 'bg-blue-500/10',
122
+ label: 'Completed'
123
+ }
124
+ }
125
+
126
+ const status = statusConfig[session.status]
127
+ const StatusIcon = status.icon
128
+
129
+ return (
130
+ <div className="bg-card border border-border rounded-lg p-4">
131
+ <div className="flex items-start justify-between mb-3">
132
+ <div className="flex-1">
133
+ <h3 className="font-medium mb-1">{session.task}</h3>
134
+ <div className="flex items-center gap-3 text-sm text-muted-foreground">
135
+ <span>{getRelativeTime(session.startedAt)}</span>
136
+ <span>•</span>
137
+ <span>{formatDuration(session.duration)}</span>
138
+ </div>
139
+ </div>
140
+
141
+ <div className={`flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium ${status.bg} ${status.color}`}>
142
+ <StatusIcon className="w-3 h-3" />
143
+ {status.label}
144
+ </div>
145
+ </div>
146
+
147
+ <div className="flex items-center gap-4 text-sm text-muted-foreground">
148
+ <span className="flex items-center gap-1">
149
+ <FileCode className="w-3.5 h-3.5" />
150
+ {session.metrics.filesChanged} files
151
+ </span>
152
+ <span className="flex items-center gap-1 text-green-500">
153
+ +{session.metrics.linesAdded}
154
+ </span>
155
+ <span className="flex items-center gap-1 text-red-500">
156
+ -{session.metrics.linesRemoved}
157
+ </span>
158
+ <span className="flex items-center gap-1">
159
+ <GitCommit className="w-3.5 h-3.5" />
160
+ {session.metrics.commits} commits
161
+ </span>
162
+ </div>
163
+ </div>
164
+ )
165
+ }