prjct-cli 0.13.3 → 0.15.0

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 (193) hide show
  1. package/CHANGELOG.md +106 -0
  2. package/bin/prjct +10 -13
  3. package/core/agentic/memory-system/semantic-memories.ts +2 -1
  4. package/core/agentic/plan-mode/plan-mode.ts +2 -1
  5. package/core/agentic/prompt-builder.ts +22 -43
  6. package/core/agentic/services.ts +5 -5
  7. package/core/agentic/smart-context.ts +7 -2
  8. package/core/command-registry/core-commands.ts +54 -29
  9. package/core/command-registry/optional-commands.ts +64 -0
  10. package/core/command-registry/setup-commands.ts +18 -3
  11. package/core/commands/analysis.ts +21 -68
  12. package/core/commands/analytics.ts +247 -213
  13. package/core/commands/base.ts +1 -1
  14. package/core/commands/index.ts +41 -36
  15. package/core/commands/maintenance.ts +300 -31
  16. package/core/commands/planning.ts +233 -22
  17. package/core/commands/setup.ts +3 -8
  18. package/core/commands/shipping.ts +14 -18
  19. package/core/commands/types.ts +8 -6
  20. package/core/commands/workflow.ts +105 -100
  21. package/core/context/generator.ts +317 -0
  22. package/core/context-sync.ts +7 -350
  23. package/core/data/index.ts +13 -32
  24. package/core/data/md-ideas-manager.ts +155 -0
  25. package/core/data/md-queue-manager.ts +4 -3
  26. package/core/data/md-shipped-manager.ts +90 -0
  27. package/core/data/md-state-manager.ts +11 -7
  28. package/core/domain/agent-generator.ts +23 -63
  29. package/core/events/index.ts +143 -0
  30. package/core/index.ts +17 -14
  31. package/core/infrastructure/capability-installer.ts +13 -149
  32. package/core/infrastructure/migrator/project-scanner.ts +2 -1
  33. package/core/infrastructure/path-manager.ts +4 -6
  34. package/core/infrastructure/setup.ts +3 -0
  35. package/core/infrastructure/uuid-migration.ts +750 -0
  36. package/core/outcomes/recorder.ts +2 -1
  37. package/core/plugin/loader.ts +4 -7
  38. package/core/plugin/registry.ts +3 -3
  39. package/core/schemas/index.ts +23 -25
  40. package/core/schemas/state.ts +1 -0
  41. package/core/serializers/ideas-serializer.ts +187 -0
  42. package/core/serializers/index.ts +16 -0
  43. package/core/serializers/shipped-serializer.ts +108 -0
  44. package/core/session/utils.ts +3 -9
  45. package/core/storage/ideas-storage.ts +273 -0
  46. package/core/storage/index.ts +204 -0
  47. package/core/storage/queue-storage.ts +297 -0
  48. package/core/storage/shipped-storage.ts +223 -0
  49. package/core/storage/state-storage.ts +235 -0
  50. package/core/storage/storage-manager.ts +175 -0
  51. package/package.json +1 -1
  52. package/packages/web/app/api/projects/[id]/momentum/route.ts +257 -0
  53. package/packages/web/app/api/sessions/current/route.ts +132 -0
  54. package/packages/web/app/api/sessions/history/route.ts +96 -14
  55. package/packages/web/app/globals.css +5 -0
  56. package/packages/web/app/layout.tsx +2 -0
  57. package/packages/web/app/project/[id]/code/layout.tsx +18 -0
  58. package/packages/web/app/project/[id]/code/page.tsx +408 -0
  59. package/packages/web/app/project/[id]/page.tsx +359 -389
  60. package/packages/web/app/project/[id]/reports/page.tsx +59 -0
  61. package/packages/web/app/project/[id]/reports/print/page.tsx +58 -0
  62. package/packages/web/components/ActivityTimeline/ActivityTimeline.tsx +0 -1
  63. package/packages/web/components/AgentsCard/AgentsCard.tsx +64 -34
  64. package/packages/web/components/AgentsCard/AgentsCard.types.ts +1 -0
  65. package/packages/web/components/AppSidebar/AppSidebar.tsx +135 -11
  66. package/packages/web/components/BentoCard/BentoCard.constants.ts +3 -3
  67. package/packages/web/components/BentoCard/BentoCard.tsx +2 -1
  68. package/packages/web/components/BentoGrid/BentoGrid.tsx +2 -2
  69. package/packages/web/components/BlockersCard/BlockersCard.tsx +65 -57
  70. package/packages/web/components/BlockersCard/BlockersCard.types.ts +1 -0
  71. package/packages/web/components/CommandBar/CommandBar.tsx +67 -0
  72. package/packages/web/components/CommandBar/index.ts +1 -0
  73. package/packages/web/components/DashboardContent/DashboardContent.tsx +35 -5
  74. package/packages/web/components/DateGroup/DateGroup.tsx +1 -1
  75. package/packages/web/components/EmptyState/EmptyState.tsx +39 -21
  76. package/packages/web/components/EmptyState/EmptyState.types.ts +1 -0
  77. package/packages/web/components/EventRow/EventRow.tsx +4 -4
  78. package/packages/web/components/EventRow/EventRow.utils.ts +3 -3
  79. package/packages/web/components/HeroSection/HeroSection.tsx +52 -15
  80. package/packages/web/components/HeroSection/HeroSection.types.ts +4 -4
  81. package/packages/web/components/HeroSection/HeroSection.utils.ts +7 -3
  82. package/packages/web/components/IdeasCard/IdeasCard.tsx +94 -27
  83. package/packages/web/components/IdeasCard/IdeasCard.types.ts +1 -0
  84. package/packages/web/components/MasonryGrid/MasonryGrid.tsx +18 -0
  85. package/packages/web/components/MasonryGrid/index.ts +1 -0
  86. package/packages/web/components/MomentumWidget/MomentumWidget.tsx +119 -0
  87. package/packages/web/components/MomentumWidget/MomentumWidget.types.ts +16 -0
  88. package/packages/web/components/MomentumWidget/index.ts +2 -0
  89. package/packages/web/components/NowCard/NowCard.tsx +81 -56
  90. package/packages/web/components/NowCard/NowCard.types.ts +1 -0
  91. package/packages/web/components/PageHeader/PageHeader.tsx +24 -0
  92. package/packages/web/components/PageHeader/index.ts +1 -0
  93. package/packages/web/components/ProgressRing/ProgressRing.constants.ts +2 -2
  94. package/packages/web/components/ProjectAvatar/ProjectAvatar.tsx +2 -2
  95. package/packages/web/components/ProjectColorDot/ProjectColorDot.tsx +37 -0
  96. package/packages/web/components/ProjectColorDot/index.ts +1 -0
  97. package/packages/web/components/ProjectSelectorModal/ProjectSelectorModal.tsx +104 -0
  98. package/packages/web/components/ProjectSelectorModal/index.ts +1 -0
  99. package/packages/web/components/Providers/Providers.tsx +4 -1
  100. package/packages/web/components/QueueCard/QueueCard.tsx +78 -25
  101. package/packages/web/components/QueueCard/QueueCard.types.ts +1 -0
  102. package/packages/web/components/QueueCard/QueueCard.utils.ts +3 -3
  103. package/packages/web/components/RecoverCard/RecoverCard.tsx +72 -0
  104. package/packages/web/components/RecoverCard/RecoverCard.types.ts +16 -0
  105. package/packages/web/components/RecoverCard/index.ts +2 -0
  106. package/packages/web/components/RoadmapCard/RoadmapCard.tsx +101 -33
  107. package/packages/web/components/RoadmapCard/RoadmapCard.types.ts +1 -0
  108. package/packages/web/components/ShipsCard/ShipsCard.tsx +71 -28
  109. package/packages/web/components/ShipsCard/ShipsCard.types.ts +2 -0
  110. package/packages/web/components/SparklineChart/SparklineChart.tsx +20 -18
  111. package/packages/web/components/StatsMasonry/StatsMasonry.tsx +95 -0
  112. package/packages/web/components/StatsMasonry/index.ts +1 -0
  113. package/packages/web/components/StreakCard/StreakCard.tsx +37 -35
  114. package/packages/web/components/TasksCounter/TasksCounter.tsx +1 -1
  115. package/packages/web/components/TechStackBadges/TechStackBadges.tsx +12 -4
  116. package/packages/web/components/TerminalDock/DockToggleTab.tsx +29 -0
  117. package/packages/web/components/TerminalDock/TerminalDock.tsx +386 -0
  118. package/packages/web/components/TerminalDock/TerminalDockTab.tsx +130 -0
  119. package/packages/web/components/TerminalDock/TerminalTabBar.tsx +142 -0
  120. package/packages/web/components/TerminalDock/index.ts +2 -0
  121. package/packages/web/components/VelocityBadge/VelocityBadge.tsx +8 -3
  122. package/packages/web/components/VelocityCard/VelocityCard.tsx +49 -47
  123. package/packages/web/components/WeeklyReports/PrintableReport.tsx +259 -0
  124. package/packages/web/components/WeeklyReports/ReportPreviewCard.tsx +187 -0
  125. package/packages/web/components/WeeklyReports/WeekCalendar.tsx +288 -0
  126. package/packages/web/components/WeeklyReports/WeeklyReports.tsx +149 -0
  127. package/packages/web/components/WeeklyReports/index.ts +4 -0
  128. package/packages/web/components/WeeklySparkline/WeeklySparkline.tsx +16 -4
  129. package/packages/web/components/WeeklySparkline/WeeklySparkline.types.ts +1 -0
  130. package/packages/web/components/charts/SessionsChart.tsx +6 -3
  131. package/packages/web/components/ui/dialog.tsx +143 -0
  132. package/packages/web/components/ui/drawer.tsx +135 -0
  133. package/packages/web/components/ui/select.tsx +187 -0
  134. package/packages/web/context/GlobalTerminalContext.tsx +538 -0
  135. package/packages/web/lib/commands.ts +81 -0
  136. package/packages/web/lib/generate-week-report.ts +285 -0
  137. package/packages/web/lib/parse-prjct-files.ts +56 -55
  138. package/packages/web/lib/project-colors.ts +58 -0
  139. package/packages/web/lib/projects.ts +58 -5
  140. package/packages/web/lib/services/projects.server.ts +11 -1
  141. package/packages/web/next-env.d.ts +1 -1
  142. package/packages/web/package.json +5 -1
  143. package/templates/commands/analyze.md +39 -3
  144. package/templates/commands/ask.md +58 -3
  145. package/templates/commands/bug.md +117 -26
  146. package/templates/commands/dash.md +95 -158
  147. package/templates/commands/done.md +130 -148
  148. package/templates/commands/feature.md +125 -103
  149. package/templates/commands/git.md +18 -3
  150. package/templates/commands/idea.md +121 -38
  151. package/templates/commands/init.md +124 -20
  152. package/templates/commands/migrate-all.md +63 -28
  153. package/templates/commands/migrate.md +140 -0
  154. package/templates/commands/next.md +115 -5
  155. package/templates/commands/now.md +146 -82
  156. package/templates/commands/pause.md +89 -74
  157. package/templates/commands/redo.md +6 -4
  158. package/templates/commands/resume.md +141 -59
  159. package/templates/commands/ship.md +103 -231
  160. package/templates/commands/spec.md +98 -8
  161. package/templates/commands/suggest.md +22 -2
  162. package/templates/commands/sync.md +192 -203
  163. package/templates/commands/undo.md +6 -4
  164. package/core/data/agents-manager.ts +0 -76
  165. package/core/data/analysis-manager.ts +0 -83
  166. package/core/data/base-manager.ts +0 -156
  167. package/core/data/ideas-manager.ts +0 -81
  168. package/core/data/outcomes-manager.ts +0 -96
  169. package/core/data/project-manager.ts +0 -75
  170. package/core/data/roadmap-manager.ts +0 -118
  171. package/core/data/shipped-manager.ts +0 -65
  172. package/core/data/state-manager.ts +0 -214
  173. package/core/state/index.ts +0 -25
  174. package/core/state/manager.ts +0 -376
  175. package/core/state/types.ts +0 -185
  176. package/core/utils/project-capabilities.ts +0 -156
  177. package/core/view-generator.ts +0 -536
  178. package/packages/web/app/project/[id]/stats/loading.tsx +0 -43
  179. package/packages/web/app/project/[id]/stats/page.tsx +0 -253
  180. package/templates/agent-assignment.md +0 -72
  181. package/templates/analysis/project-analysis.md +0 -78
  182. package/templates/checklists/accessibility.md +0 -33
  183. package/templates/commands/build.md +0 -17
  184. package/templates/commands/decision.md +0 -226
  185. package/templates/commands/fix.md +0 -79
  186. package/templates/commands/help.md +0 -61
  187. package/templates/commands/progress.md +0 -14
  188. package/templates/commands/recap.md +0 -14
  189. package/templates/commands/roadmap.md +0 -52
  190. package/templates/commands/status.md +0 -17
  191. package/templates/commands/task.md +0 -63
  192. package/templates/commands/work.md +0 -44
  193. package/templates/commands/workflow.md +0 -12
@@ -1,414 +1,384 @@
1
- 'use client'
2
-
3
- import { useState, use } from 'react'
4
- import { useRouter } from 'next/navigation'
5
- import { useProject, useDeleteProject } from '@/hooks/useProjects'
6
- import { TerminalTabsProvider, useTerminalTabs } from '@/context/TerminalTabsContext'
7
- import { TerminalTabs } from '@/components/TerminalTabs'
8
- import { Button } from '@/components/ui/button'
9
- import { Badge } from '@/components/ui/badge'
10
- import { TooltipProvider } from '@/components/ui/tooltip'
11
- import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
1
+ import { notFound } from 'next/navigation'
2
+ import type { Metadata } from 'next'
12
3
  import {
13
- AlertDialog,
14
- AlertDialogAction,
15
- AlertDialogCancel,
16
- AlertDialogContent,
17
- AlertDialogDescription,
18
- AlertDialogFooter,
19
- AlertDialogHeader,
20
- AlertDialogTitle,
21
- } from '@/components/ui/alert-dialog'
22
- import { ProjectAvatar } from '@/components/ProjectAvatar'
23
- import { TechStackBadges } from '@/components/TechStackBadges'
24
- import { CommandButton } from '@/components/CommandButton'
25
- import { formatPath } from '@/lib/format'
26
- import {
27
- Loader2,
28
- Play,
29
- Target,
30
- Lightbulb,
31
- ListTodo,
32
- Rocket,
33
- Sparkles,
34
- CheckCircle2,
35
- AlertTriangle,
36
- Trash2,
37
- ArrowLeft,
38
- FolderGit2,
39
- Pause,
40
- BarChart3,
41
- TrendingUp,
42
- Activity,
43
- History,
44
- Undo2,
45
- Redo2,
46
- Command,
47
- X,
48
- RefreshCw
49
- } from 'lucide-react'
50
- import { cn } from '@/lib/utils'
51
-
52
- // Commands ordered by real developer workflow
53
- const WORKFLOW_COMMANDS = [
54
- { cmd: 'p. now', icon: Target, tip: 'Set task', group: 'work' },
55
- { cmd: 'p. done', icon: CheckCircle2, tip: 'Complete', group: 'work' },
56
- { cmd: 'p. pause', icon: Pause, tip: 'Pause', group: 'session' },
57
- { cmd: 'p. resume', icon: Play, tip: 'Resume', group: 'session' },
58
- { cmd: 'p. feature', icon: Sparkles, tip: 'Feature', group: 'plan' },
59
- { cmd: 'p. idea', icon: Lightbulb, tip: 'Idea', group: 'plan' },
60
- { cmd: 'p. next', icon: ListTodo, tip: 'Queue', group: 'plan' },
61
- { cmd: 'p. ship', icon: Rocket, tip: 'Ship', group: 'ship' },
62
- { cmd: 'p. recap', icon: BarChart3, tip: 'Recap', group: 'status' },
63
- { cmd: 'p. progress', icon: TrendingUp, tip: 'Progress', group: 'status' },
64
- { cmd: 'p. status', icon: Activity, tip: 'Status', group: 'status' },
65
- { cmd: 'p. history', icon: History, tip: 'History', group: 'status' },
66
- { cmd: 'p. undo', icon: Undo2, tip: 'Undo', group: 'recovery' },
67
- { cmd: 'p. redo', icon: Redo2, tip: 'Redo', group: 'recovery' },
68
- ] as const
69
-
70
- const COMMAND_GROUPS = ['work', 'session', 'plan', 'ship', 'status', 'recovery'] as const
71
-
72
- // Command sidebar content - shared between desktop and mobile
73
- function CommandSidebarContent({
74
- projectId,
75
- project,
76
- isActiveConnected,
77
- sendCommandToActive,
78
- onCommandClick
79
- }: {
80
- projectId: string
81
- project: { name?: string; iconPath?: string | null }
82
- isActiveConnected: boolean
83
- sendCommandToActive: (cmd: string) => void
84
- onCommandClick?: () => void
85
- }) {
86
- const router = useRouter()
87
-
88
- const handleCommand = (cmd: string) => {
89
- sendCommandToActive(cmd)
90
- onCommandClick?.()
4
+ getStats,
5
+ getInsightMessage,
6
+ calculateStreak,
7
+ getVelocityChange,
8
+ getWeeklyVelocityData,
9
+ type StatsResult
10
+ } from '@/lib/services/stats.server'
11
+ import { getProject } from '@/lib/services/projects.server'
12
+ import { getProjectEmoji } from '@/lib/project-colors'
13
+ import type { TimelineEvent } from '@/lib/parse-prjct-files'
14
+
15
+ import { HeroSection } from '@/components/HeroSection'
16
+ import { StatsMasonry } from '@/components/StatsMasonry'
17
+
18
+ // Dynamic metadata for browser tab title
19
+ export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
20
+ const { id: projectId } = await params
21
+ const project = await getProject(projectId)
22
+ const projectName = project?.name ?? projectId
23
+ const emoji = getProjectEmoji(projectId)
24
+
25
+ return {
26
+ title: `${emoji} ${projectName} / p.`,
91
27
  }
28
+ }
92
29
 
93
- return (
94
- <>
95
- <div className="h-14 flex items-center justify-center border-b border-border w-full">
96
- <ProjectAvatar
97
- projectId={projectId}
98
- name={project.name || projectId}
99
- iconPath={project.iconPath}
100
- />
101
- </div>
30
+ // Types for normalized component data
31
+ interface NormalizedCurrentTask {
32
+ task: string
33
+ startedAt?: string
34
+ agent?: string
35
+ estimatedDuration?: string
36
+ pausedAt?: string
37
+ pauseReason?: string
38
+ }
102
39
 
103
- <div className="flex-1 flex flex-col gap-1 overflow-auto py-3">
104
- {/* Stats button - navigates to stats page */}
105
- <CommandButton
106
- cmd="Project stats"
107
- icon={BarChart3}
108
- tip="Stats"
109
- disabled={false}
110
- onClick={() => {
111
- router.push(`/project/${projectId}/stats`)
112
- onCommandClick?.()
113
- }}
114
- />
115
- {/* Sync button - prominent, always visible */}
116
- <CommandButton
117
- cmd="p. sync"
118
- icon={RefreshCw}
119
- tip="Sync"
120
- disabled={!isActiveConnected}
121
- onClick={() => handleCommand('p. sync')}
122
- variant="primary"
123
- />
124
- <div className="border-b border-border w-8 my-2 mx-auto" />
125
-
126
- {COMMAND_GROUPS.map((group, groupIndex) => (
127
- <div key={group} className="flex flex-col items-center">
128
- {WORKFLOW_COMMANDS.filter(c => c.group === group).map(({ cmd, icon, tip }) => (
129
- <CommandButton
130
- key={cmd}
131
- cmd={cmd}
132
- icon={icon}
133
- tip={tip}
134
- disabled={!isActiveConnected}
135
- onClick={() => handleCommand(cmd)}
136
- />
137
- ))}
138
- {groupIndex < COMMAND_GROUPS.length - 1 && (
139
- <div className="border-b border-border w-8 my-2" />
140
- )}
141
- </div>
142
- ))}
143
- </div>
144
- </>
145
- )
40
+ interface NormalizedQueueItem {
41
+ task: string
42
+ priority?: 'low' | 'medium' | 'high' | 'critical' | number
43
+ estimatedDuration?: string
146
44
  }
147
45
 
148
- // Inner component that uses the terminal context
149
- function ProjectPageContent({ projectId, project }: { projectId: string; project: NonNullable<ReturnType<typeof useProject>['data']> }) {
150
- const router = useRouter()
151
- const [commandSheetOpen, setCommandSheetOpen] = useState(false)
46
+ interface NormalizedShip {
47
+ name: string
48
+ date: string
49
+ duration?: string
50
+ }
152
51
 
153
- const {
154
- sessions,
155
- sendCommandToActive,
156
- getActiveSession
157
- } = useTerminalTabs()
52
+ interface NormalizedIdea {
53
+ title: string
54
+ impact?: string
55
+ }
158
56
 
159
- const activeSession = getActiveSession()
160
- const hasActiveSessions = sessions.length > 0
161
- const isActiveConnected = activeSession?.isConnected ?? false
57
+ interface NormalizedAgent {
58
+ name: string
59
+ description?: string
60
+ successRate?: number
61
+ tasksCompleted?: number
62
+ bestFor?: string[]
63
+ }
162
64
 
163
- return (
164
- <div className="h-full">
165
- <TooltipProvider>
166
- <div className="flex h-full">
167
- {/* Desktop Sidebar - hidden on mobile */}
168
- <aside className="hidden md:flex w-14 border-r border-border flex-col bg-card/50 items-center">
169
- <CommandSidebarContent
170
- projectId={projectId}
171
- project={project}
172
- isActiveConnected={isActiveConnected}
173
- sendCommandToActive={sendCommandToActive}
174
- />
175
- </aside>
176
-
177
- {/* Main */}
178
- <main className="flex-1 flex flex-col min-w-0">
179
- {/* Header - Responsive */}
180
- <header className="h-auto md:h-14 flex flex-col md:flex-row md:items-center justify-between px-3 md:px-4 py-2 md:py-0 border-b border-border bg-card gap-2">
181
- {/* Mobile: Add padding for hamburger menu */}
182
- <div className="flex items-center gap-3 pl-12 md:pl-0">
183
- {/* Mobile: Show project avatar */}
184
- <div className="md:hidden">
185
- <ProjectAvatar
186
- projectId={projectId}
187
- name={project.name || projectId}
188
- iconPath={project.iconPath}
189
- size="sm"
190
- />
191
- </div>
192
- <div className="flex flex-col min-w-0">
193
- <div className="flex items-center gap-2">
194
- <span className="font-bold leading-tight truncate">{project.name || projectId}</span>
195
- {project.version && (
196
- <Badge variant="outline" className="text-[10px] px-1.5 py-0 font-mono shrink-0">
197
- v{project.version}
198
- </Badge>
199
- )}
200
- </div>
201
- {project.repoPath && (
202
- <span className="text-xs text-muted-foreground leading-tight flex items-center gap-1 truncate">
203
- <FolderGit2 className="w-3 h-3 shrink-0" />
204
- <span className="truncate">{formatPath(project.repoPath)}</span>
205
- </span>
206
- )}
207
- </div>
208
- {hasActiveSessions && (
209
- <Badge variant="outline" className="text-green-500 border-green-500/50 shrink-0">
210
- {sessions.filter(s => s.isConnected).length} active
211
- </Badge>
212
- )}
213
- </div>
214
-
215
- {/* Desktop only: metadata and tech stack */}
216
- <div className="hidden md:flex items-center gap-4">
217
- <div className="flex items-center gap-3 text-xs text-muted-foreground">
218
- {project.stack && <span>{project.stack}</span>}
219
- {project.filesCount && (
220
- <span><span className="font-medium text-foreground">{project.filesCount}</span> files</span>
221
- )}
222
- {project.commitsCount && (
223
- <span><span className="font-medium text-foreground">{project.commitsCount}</span> commits</span>
224
- )}
225
- </div>
226
- <TechStackBadges techStack={project.techStack || []} />
227
- </div>
228
- </header>
229
-
230
- {/* Terminal tabs area */}
231
- <div className="flex-1 min-h-0">
232
- <TerminalTabs projectDir={project.repoPath || project.path || '/tmp'} />
233
- </div>
234
- </main>
235
- </div>
65
+ interface NormalizedRoadmap {
66
+ phases: Array<{
67
+ name: string
68
+ progress: number
69
+ features?: Array<{ name: string; status: string }>
70
+ }>
71
+ progress: number
72
+ }
236
73
 
237
- {/* Mobile: Floating Action Button for commands */}
238
- <div className="md:hidden fixed bottom-4 right-4 z-50">
239
- <Sheet open={commandSheetOpen} onOpenChange={setCommandSheetOpen}>
240
- <SheetTrigger asChild>
241
- <button
242
- className={cn(
243
- "h-14 w-14 rounded-full shadow-lg flex items-center justify-center transition-all",
244
- isActiveConnected
245
- ? "bg-primary text-primary-foreground hover:bg-primary/90"
246
- : "bg-muted text-muted-foreground"
247
- )}
248
- aria-label="Open commands"
249
- >
250
- <Command className="h-6 w-6" />
251
- </button>
252
- </SheetTrigger>
253
- <SheetContent side="bottom" className="h-[70vh] rounded-t-2xl px-0">
254
- <div className="flex flex-col h-full">
255
- <div className="px-4 pb-2 border-b border-border">
256
- <div className="flex items-center justify-between">
257
- <h3 className="font-semibold">Commands</h3>
258
- <Badge variant={isActiveConnected ? "default" : "secondary"}>
259
- {isActiveConnected ? "Connected" : "Disconnected"}
260
- </Badge>
261
- </div>
262
- </div>
263
-
264
- {/* Command grid for mobile */}
265
- <div className="flex-1 overflow-auto p-4">
266
- <div className="grid grid-cols-4 gap-3">
267
- {/* Stats button */}
268
- <button
269
- onClick={() => {
270
- router.push(`/project/${projectId}/stats`)
271
- setCommandSheetOpen(false)
272
- }}
273
- className="flex flex-col items-center gap-1.5 p-3 rounded-lg hover:bg-accent transition-colors"
274
- >
275
- <div className="h-10 w-10 rounded-full bg-accent flex items-center justify-center">
276
- <BarChart3 className="h-5 w-5" />
277
- </div>
278
- <span className="text-xs text-muted-foreground">Stats</span>
279
- </button>
280
-
281
- {/* Sync button - prominent */}
282
- <button
283
- onClick={() => {
284
- sendCommandToActive('p. sync')
285
- setCommandSheetOpen(false)
286
- }}
287
- disabled={!isActiveConnected}
288
- className={cn(
289
- "flex flex-col items-center gap-1.5 p-3 rounded-lg transition-colors",
290
- isActiveConnected
291
- ? "hover:bg-primary/10"
292
- : "opacity-50 cursor-not-allowed"
293
- )}
294
- >
295
- <div className={cn(
296
- "h-10 w-10 rounded-full flex items-center justify-center",
297
- isActiveConnected ? "bg-primary text-primary-foreground" : "bg-muted"
298
- )}>
299
- <RefreshCw className="h-5 w-5" />
300
- </div>
301
- <span className="text-xs text-primary font-medium">Sync</span>
302
- </button>
303
-
304
- {WORKFLOW_COMMANDS.map(({ cmd, icon: Icon, tip }) => (
305
- <button
306
- key={cmd}
307
- onClick={() => {
308
- sendCommandToActive(cmd)
309
- setCommandSheetOpen(false)
310
- }}
311
- disabled={!isActiveConnected}
312
- className={cn(
313
- "flex flex-col items-center gap-1.5 p-3 rounded-lg transition-colors",
314
- isActiveConnected
315
- ? "hover:bg-accent"
316
- : "opacity-50 cursor-not-allowed"
317
- )}
318
- >
319
- <div className={cn(
320
- "h-10 w-10 rounded-full flex items-center justify-center",
321
- isActiveConnected ? "bg-accent" : "bg-muted"
322
- )}>
323
- <Icon className="h-5 w-5" />
324
- </div>
325
- <span className="text-xs text-muted-foreground">{tip}</span>
326
- </button>
327
- ))}
328
- </div>
329
- </div>
330
- </div>
331
- </SheetContent>
332
- </Sheet>
333
- </div>
334
- </TooltipProvider>
335
- </div>
74
+ // Data normalization functions
75
+ function normalizeCurrentTask(stats: StatsResult): NormalizedCurrentTask | null {
76
+ if (stats.state?.currentTask) {
77
+ return {
78
+ task: stats.state.currentTask.description,
79
+ startedAt: stats.state.currentTask.startedAt,
80
+ // Simplified - removed legacy fields not in new schema
81
+ }
82
+ }
83
+ return stats.legacyStats?.currentTask ?? null
84
+ }
85
+
86
+ function normalizeQueue(stats: StatsResult): NormalizedQueueItem[] {
87
+ if (stats.queue?.tasks) {
88
+ return stats.queue.tasks
89
+ .filter(t => !t.completed)
90
+ .map(q => ({
91
+ task: q.description,
92
+ priority: q.priority,
93
+ }))
94
+ }
95
+ return stats.legacyStats?.queue ?? []
96
+ }
97
+
98
+ function normalizeRoadmap(stats: StatsResult): NormalizedRoadmap | null {
99
+ const features = stats.roadmap?.features ?? []
100
+ if (features.length > 0) {
101
+ const completed = features.filter(f =>
102
+ f.status === 'shipped' || f.status === 'completed'
103
+ ).length
104
+
105
+ return {
106
+ phases: features.map(f => ({
107
+ name: f.name,
108
+ progress: f.status === 'shipped' || f.status === 'completed' ? 100 :
109
+ f.status === 'active' ? 50 : 0,
110
+ features: f.tasks.map(t => ({
111
+ name: t.description,
112
+ status: t.completed ? 'completed' : 'pending'
113
+ }))
114
+ })),
115
+ progress: Math.round((completed / features.length) * 100)
116
+ }
117
+ }
118
+ return stats.legacyStats?.roadmap ?? null
119
+ }
120
+
121
+ function normalizeShipped(stats: StatsResult): NormalizedShip[] {
122
+ // Try new format first
123
+ const items = stats.shipped?.items ?? []
124
+ if (items.length > 0) {
125
+ return items.map(s => ({
126
+ name: s.name,
127
+ date: s.shippedAt || s.date || new Date().toISOString(),
128
+ }))
129
+ }
130
+ // Fallback to legacy
131
+ return (stats.legacyStats?.shipped ?? []).map(s => ({
132
+ name: s.name,
133
+ date: s.date,
134
+ duration: s.time,
135
+ }))
136
+ }
137
+
138
+ function normalizeIdeas(stats: StatsResult): NormalizedIdea[] {
139
+ // Try new format first
140
+ const ideas = stats.ideas?.ideas ?? []
141
+ if (ideas.length > 0) {
142
+ return ideas
143
+ .filter(i => i.status === 'pending')
144
+ .map(i => ({
145
+ title: i.text,
146
+ impact: i.priority?.toUpperCase() || 'MEDIUM'
147
+ }))
148
+ }
149
+ // Fallback to legacy
150
+ return (stats.legacyStats?.ideas?.pending ?? []).map(i => ({
151
+ title: i.title,
152
+ impact: i.impact?.toUpperCase() || 'MEDIUM'
153
+ }))
154
+ }
155
+
156
+ function normalizeAgents(stats: StatsResult): NormalizedAgent[] {
157
+ // Try new format first
158
+ if (stats.agents.length > 0) {
159
+ return stats.agents.map(a => ({
160
+ name: a.name,
161
+ description: a.description,
162
+ successRate: a.successRate,
163
+ tasksCompleted: a.tasksCompleted,
164
+ bestFor: a.bestFor,
165
+ }))
166
+ }
167
+ // Fallback to legacy
168
+ return (stats.legacyStats?.agents ?? []).map(a => ({
169
+ name: a.name,
170
+ description: a.role,
171
+ bestFor: a.whenToUse,
172
+ }))
173
+ }
174
+
175
+ function normalizeTimeline(stats: StatsResult): TimelineEvent[] {
176
+ if (stats.metrics?.recentActivity?.length) {
177
+ return stats.metrics.recentActivity.map(a => ({
178
+ ts: a.timestamp,
179
+ type: a.action || a.type || 'task_completed',
180
+ task: a.description || '',
181
+ }))
182
+ }
183
+ return stats.legacyStats?.timeline ?? []
184
+ }
185
+
186
+ function getVelocity(stats: StatsResult): number {
187
+ if (stats.metrics?.velocity?.tasksPerDay) {
188
+ return stats.metrics.velocity.tasksPerDay
189
+ }
190
+ return stats.legacyStats?.metrics?.velocity?.tasksPerDay ?? 0
191
+ }
192
+
193
+ function getTotalShips(stats: StatsResult): number {
194
+ // Use shipped.md items count (legacyStats.shipped) as source of truth
195
+ return stats.shipped?.items?.length ?? stats.legacyStats?.shipped?.length ?? 0
196
+ }
197
+
198
+ function getCompletionRate(stats: StatsResult): number {
199
+ const completed = stats.legacyStats?.shipped?.length ?? 0
200
+ const pending = stats.legacyStats?.queue?.length ?? 0
201
+ const total = completed + pending
202
+ if (total === 0) return 0
203
+ return Math.round((completed / total) * 100)
204
+ }
205
+
206
+ // Calculate streak from legacy timeline
207
+ function calculateStreakFromTimeline(stats: StatsResult): number {
208
+ const timeline = stats.legacyStats?.timeline ?? []
209
+ if (timeline.length === 0) return 0
210
+
211
+ const activityDates = new Set(
212
+ timeline.map(e => e.ts?.split('T')[0]).filter(Boolean)
336
213
  )
214
+
215
+ const today = new Date()
216
+ today.setHours(0, 0, 0, 0)
217
+
218
+ const days = Array.from({ length: 30 }, (_, i) => {
219
+ const date = new Date(today)
220
+ date.setDate(date.getDate() - i)
221
+ return date.toISOString().split('T')[0]
222
+ })
223
+
224
+ const firstGapIndex = days.findIndex(date => !activityDates.has(date))
225
+
226
+ if (firstGapIndex === 0) {
227
+ const yesterdayHasActivity = activityDates.has(days[1])
228
+ if (!yesterdayHasActivity) return 0
229
+ const remainingDays = days.slice(1)
230
+ const gapFromYesterday = remainingDays.findIndex(date => !activityDates.has(date))
231
+ return gapFromYesterday === -1 ? remainingDays.length : gapFromYesterday
232
+ }
233
+
234
+ return firstGapIndex === -1 ? days.length : firstGapIndex
337
235
  }
338
236
 
339
- export default function ProjectPage({ params }: { params: Promise<{ id: string }> }) {
340
- const { id: projectId } = use(params)
341
- const router = useRouter()
342
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
237
+ // Get weekly velocity data from legacy timeline
238
+ function getWeeklyDataFromTimeline(stats: StatsResult): number[] {
239
+ const timeline = stats.legacyStats?.timeline ?? []
240
+ if (timeline.length === 0) return []
343
241
 
344
- const { data: project, isLoading: projectLoading } = useProject(projectId)
345
- const deleteMutation = useDeleteProject()
242
+ const today = new Date()
346
243
 
347
- if (projectLoading) {
348
- return (
349
- <div className="flex items-center justify-center h-full">
350
- <Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
351
- </div>
352
- )
244
+ return Array.from({ length: 7 }, (_, i) => {
245
+ const date = new Date(today)
246
+ date.setDate(date.getDate() - (6 - i))
247
+ const dateStr = date.toISOString().split('T')[0]
248
+
249
+ return timeline.filter(e => e.ts?.startsWith(dateStr)).length
250
+ })
251
+ }
252
+
253
+ interface PageProps {
254
+ params: Promise<{ id: string }>
255
+ }
256
+
257
+ export default async function ProjectStatsPage({ params }: PageProps) {
258
+ const { id: projectId } = await params
259
+
260
+ // Fetch data directly on server - no API calls
261
+ const [project, stats] = await Promise.all([
262
+ getProject(projectId),
263
+ getStats(projectId)
264
+ ])
265
+
266
+ if (!stats.hasData) {
267
+ notFound()
353
268
  }
354
269
 
355
- if (!project) {
356
- return (
357
- <div className="flex items-center justify-center h-full p-4">
358
- <div className="text-center space-y-4 max-w-sm">
359
- <div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center mx-auto">
360
- <AlertTriangle className="w-8 h-8 text-muted-foreground" />
270
+ // Compute derived values using service functions
271
+ // Use legacy timeline for streak/weekly if metrics is empty
272
+ const streak = stats.metrics?.recentActivity?.length
273
+ ? calculateStreak(stats.metrics)
274
+ : calculateStreakFromTimeline(stats)
275
+ const velocity = getVelocity(stats)
276
+ const velocityChange = getVelocityChange(velocity)
277
+ const insightMessage = getInsightMessage(stats, streak)
278
+ const weeklyVelocityData = stats.metrics?.recentActivity?.length
279
+ ? getWeeklyVelocityData(stats.metrics)
280
+ : getWeeklyDataFromTimeline(stats)
281
+
282
+ // Normalize data for components
283
+ const currentTask = normalizeCurrentTask(stats)
284
+ const queue = normalizeQueue(stats)
285
+ const roadmap = normalizeRoadmap(stats)
286
+ const shipped = normalizeShipped(stats)
287
+ const ideas = normalizeIdeas(stats)
288
+ const agents = normalizeAgents(stats)
289
+ const timeline = normalizeTimeline(stats)
290
+
291
+ // DRY: Use counts from getProject() - same source as dashboard
292
+ const totalShips = project?.shippedCount ?? 0
293
+ const completionRate = project?.completionRate ?? 0
294
+
295
+ // Extract insights
296
+ const { estimateAccuracy, blockers } = stats.insights
297
+
298
+ return (
299
+ <div className="flex h-full flex-col p-4 md:p-6 overflow-auto overflow-x-hidden">
300
+ {/* Mobile: Add padding for hamburger menu */}
301
+ <div className="pl-10 md:pl-0">
302
+ <HeroSection
303
+ projectId={projectId}
304
+ projectName={project?.name ?? projectId}
305
+ projectVersion={project?.version}
306
+ totalShips={totalShips}
307
+ completionRate={completionRate}
308
+ streak={streak}
309
+ insightMessage={insightMessage}
310
+ timeline={timeline}
311
+ />
312
+ </div>
313
+
314
+ {/* Quick Stats Bar */}
315
+ <div className="grid grid-cols-2 sm:grid-cols-4 gap-2 sm:gap-3 mt-4 mb-6">
316
+ <div className="bg-card border rounded-lg p-2 sm:p-3 flex items-center gap-2 sm:gap-3">
317
+ <div className="h-8 w-8 sm:h-10 sm:w-10 rounded-lg bg-muted flex items-center justify-center shrink-0">
318
+ <svg className="h-4 w-4 sm:h-5 sm:w-5 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor">
319
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 3l14 9-14 9V3z" />
320
+ </svg>
361
321
  </div>
362
- <div>
363
- <h2 className="text-lg font-medium">Project not found</h2>
364
- <p className="text-sm text-muted-foreground mt-1 break-all">ID: {projectId}</p>
322
+ <div className="min-w-0">
323
+ <div className="text-xl sm:text-2xl font-bold tabular-nums">{totalShips}</div>
324
+ <div className="text-xs text-muted-foreground truncate">Shipped</div>
365
325
  </div>
366
- <div className="flex flex-col sm:flex-row gap-2 justify-center">
367
- <Button variant="outline" onClick={() => router.push('/')}>
368
- <ArrowLeft className="w-4 h-4 mr-2" />
369
- Back to Dashboard
370
- </Button>
371
- <Button variant="destructive" onClick={() => setShowDeleteConfirm(true)}>
372
- <Trash2 className="w-4 h-4 mr-2" />
373
- Delete from Storage
374
- </Button>
326
+ </div>
327
+ <div className="bg-card border rounded-lg p-2 sm:p-3 flex items-center gap-2 sm:gap-3">
328
+ <div className="h-8 w-8 sm:h-10 sm:w-10 rounded-lg bg-muted flex items-center justify-center shrink-0">
329
+ <svg className="h-4 w-4 sm:h-5 sm:w-5 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor">
330
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
331
+ </svg>
332
+ </div>
333
+ <div className="min-w-0">
334
+ <div className="text-xl sm:text-2xl font-bold tabular-nums">{queue.length}</div>
335
+ <div className="text-xs text-muted-foreground truncate">Queue</div>
336
+ </div>
337
+ </div>
338
+ <div className="bg-card border rounded-lg p-2 sm:p-3 flex items-center gap-2 sm:gap-3">
339
+ <div className="h-8 w-8 sm:h-10 sm:w-10 rounded-lg bg-muted flex items-center justify-center shrink-0">
340
+ <svg className="h-4 w-4 sm:h-5 sm:w-5 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor">
341
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17.657 18.657A8 8 0 016.343 7.343S7 9 9 10c0-2 .5-5 2.986-7C14 5 16.09 5.777 17.656 7.343A7.975 7.975 0 0120 13a7.975 7.975 0 01-2.343 5.657z" />
342
+ </svg>
343
+ </div>
344
+ <div className="min-w-0">
345
+ <div className="text-xl sm:text-2xl font-bold tabular-nums">{streak}</div>
346
+ <div className="text-xs text-muted-foreground truncate">Streak</div>
347
+ </div>
348
+ </div>
349
+ <div className="bg-card border rounded-lg p-2 sm:p-3 flex items-center gap-2 sm:gap-3">
350
+ <div className="h-8 w-8 sm:h-10 sm:w-10 rounded-lg bg-muted flex items-center justify-center shrink-0">
351
+ <svg className="h-4 w-4 sm:h-5 sm:w-5 text-muted-foreground" fill="none" viewBox="0 0 24 24" stroke="currentColor">
352
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
353
+ </svg>
354
+ </div>
355
+ <div className="min-w-0">
356
+ <div className="text-xl sm:text-2xl font-bold tabular-nums">{ideas.length}</div>
357
+ <div className="text-xs text-muted-foreground truncate">Ideas</div>
375
358
  </div>
376
-
377
- <AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
378
- <AlertDialogContent className="max-w-[calc(100vw-2rem)] sm:max-w-lg">
379
- <AlertDialogHeader>
380
- <AlertDialogTitle className="flex items-center gap-2">
381
- <AlertTriangle className="w-5 h-5 text-destructive" />
382
- Delete Project Data?
383
- </AlertDialogTitle>
384
- <AlertDialogDescription>
385
- This will move the project storage to trash.
386
- <br />
387
- <span className="text-muted-foreground text-sm break-all">ID: {projectId}</span>
388
- </AlertDialogDescription>
389
- </AlertDialogHeader>
390
- <AlertDialogFooter className="flex-col sm:flex-row gap-2">
391
- <AlertDialogCancel disabled={deleteMutation.isPending} className="w-full sm:w-auto">
392
- Cancel
393
- </AlertDialogCancel>
394
- <AlertDialogAction
395
- onClick={() => deleteMutation.mutate(projectId, { onSuccess: () => router.push('/') })}
396
- disabled={deleteMutation.isPending}
397
- className="bg-destructive text-destructive-foreground hover:bg-destructive/90 w-full sm:w-auto"
398
- >
399
- {deleteMutation.isPending ? 'Deleting...' : 'Delete'}
400
- </AlertDialogAction>
401
- </AlertDialogFooter>
402
- </AlertDialogContent>
403
- </AlertDialog>
404
359
  </div>
405
360
  </div>
406
- )
407
- }
408
361
 
409
- return (
410
- <TerminalTabsProvider projectId={projectId}>
411
- <ProjectPageContent projectId={projectId} project={project} />
412
- </TerminalTabsProvider>
362
+ {/* Main Content - Masonry Layout */}
363
+ <StatsMasonry
364
+ projectId={projectId}
365
+ currentTask={currentTask}
366
+ velocity={velocity}
367
+ weeklyVelocityData={weeklyVelocityData}
368
+ velocityChange={velocityChange}
369
+ estimateAccuracy={estimateAccuracy}
370
+ roadmap={roadmap}
371
+ queue={queue}
372
+ shipped={shipped}
373
+ totalShips={totalShips}
374
+ streak={streak}
375
+ blockers={blockers}
376
+ ideas={ideas}
377
+ agents={agents}
378
+ timeline={timeline}
379
+ />
380
+
381
+ <div className="h-4" />
382
+ </div>
413
383
  )
414
384
  }