prjct-cli 0.13.3 → 0.15.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 (195) hide show
  1. package/CHANGELOG.md +122 -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/setup.md +18 -3
  160. package/templates/commands/ship.md +103 -231
  161. package/templates/commands/spec.md +98 -8
  162. package/templates/commands/suggest.md +22 -2
  163. package/templates/commands/sync.md +192 -203
  164. package/templates/commands/undo.md +6 -4
  165. package/templates/mcp-config.json +20 -1
  166. package/core/data/agents-manager.ts +0 -76
  167. package/core/data/analysis-manager.ts +0 -83
  168. package/core/data/base-manager.ts +0 -156
  169. package/core/data/ideas-manager.ts +0 -81
  170. package/core/data/outcomes-manager.ts +0 -96
  171. package/core/data/project-manager.ts +0 -75
  172. package/core/data/roadmap-manager.ts +0 -118
  173. package/core/data/shipped-manager.ts +0 -65
  174. package/core/data/state-manager.ts +0 -214
  175. package/core/state/index.ts +0 -25
  176. package/core/state/manager.ts +0 -376
  177. package/core/state/types.ts +0 -185
  178. package/core/utils/project-capabilities.ts +0 -156
  179. package/core/view-generator.ts +0 -536
  180. package/packages/web/app/project/[id]/stats/loading.tsx +0 -43
  181. package/packages/web/app/project/[id]/stats/page.tsx +0 -253
  182. package/templates/agent-assignment.md +0 -72
  183. package/templates/analysis/project-analysis.md +0 -78
  184. package/templates/checklists/accessibility.md +0 -33
  185. package/templates/commands/build.md +0 -17
  186. package/templates/commands/decision.md +0 -226
  187. package/templates/commands/fix.md +0 -79
  188. package/templates/commands/help.md +0 -61
  189. package/templates/commands/progress.md +0 -14
  190. package/templates/commands/recap.md +0 -14
  191. package/templates/commands/roadmap.md +0 -52
  192. package/templates/commands/status.md +0 -17
  193. package/templates/commands/task.md +0 -63
  194. package/templates/commands/work.md +0 -44
  195. package/templates/commands/workflow.md +0 -12
@@ -0,0 +1,285 @@
1
+ import type { StatsResult } from './services/stats.server'
2
+
3
+ export interface ShippedItem {
4
+ name: string
5
+ date: string
6
+ version?: string
7
+ type?: string
8
+ }
9
+
10
+ export interface WeekData {
11
+ year: number
12
+ week: number
13
+ startDate: Date
14
+ endDate: Date
15
+ shipped: ShippedItem[]
16
+ tasksCompleted: number
17
+ bugsFixed: number
18
+ syncs: number
19
+ activeDays: number
20
+ }
21
+
22
+ /**
23
+ * Get ISO week number for a date
24
+ */
25
+ export function getWeekNumber(date: Date): number {
26
+ const d = new Date(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()))
27
+ const dayNum = d.getUTCDay() || 7
28
+ d.setUTCDate(d.getUTCDate() + 4 - dayNum)
29
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
30
+ return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7)
31
+ }
32
+
33
+ /**
34
+ * Get current year and week
35
+ */
36
+ export function getCurrentYearWeek(): { year: number; week: number } {
37
+ const now = new Date()
38
+ return {
39
+ year: now.getFullYear(),
40
+ week: getWeekNumber(now)
41
+ }
42
+ }
43
+
44
+ /**
45
+ * Get start and end dates for a given ISO week
46
+ */
47
+ export function getWeekDateRange(year: number, week: number): { start: Date; end: Date } {
48
+ // Find January 4th of the year (always in week 1)
49
+ const jan4 = new Date(year, 0, 4)
50
+ const dayOfWeek = jan4.getDay() || 7
51
+
52
+ // Find Monday of week 1
53
+ const week1Monday = new Date(jan4)
54
+ week1Monday.setDate(jan4.getDate() - dayOfWeek + 1)
55
+
56
+ // Calculate target week's Monday
57
+ const targetMonday = new Date(week1Monday)
58
+ targetMonday.setDate(week1Monday.getDate() + (week - 1) * 7)
59
+
60
+ // Calculate Sunday
61
+ const targetSunday = new Date(targetMonday)
62
+ targetSunday.setDate(targetMonday.getDate() + 6)
63
+
64
+ return { start: targetMonday, end: targetSunday }
65
+ }
66
+
67
+ /**
68
+ * Format date range as string
69
+ */
70
+ export function formatDateRange(start: Date, end: Date): string {
71
+ const startMonth = start.toLocaleDateString('en-US', { month: 'short' })
72
+ const endMonth = end.toLocaleDateString('en-US', { month: 'short' })
73
+
74
+ if (startMonth === endMonth) {
75
+ return `${startMonth} ${start.getDate()}-${end.getDate()}`
76
+ }
77
+ return `${startMonth} ${start.getDate()} - ${endMonth} ${end.getDate()}`
78
+ }
79
+
80
+ /**
81
+ * Check if a date falls within a week
82
+ */
83
+ function isDateInWeek(dateStr: string, weekStart: Date, weekEnd: Date): boolean {
84
+ const date = new Date(dateStr)
85
+ return date >= weekStart && date <= weekEnd
86
+ }
87
+
88
+ /**
89
+ * Filter stats data by week
90
+ */
91
+ export function filterDataByWeek(
92
+ stats: StatsResult,
93
+ year: number,
94
+ week: number
95
+ ): WeekData {
96
+ const { start, end } = getWeekDateRange(year, week)
97
+
98
+ // 1. Shipped features from legacyStats.shipped (parsed from shipped.md)
99
+ const shipped: ShippedItem[] = (stats.legacyStats?.shipped ?? [])
100
+ .filter(item => {
101
+ if (!item.date) return false
102
+ return isDateInWeek(item.date, start, end)
103
+ })
104
+ .map(item => ({
105
+ name: item.name,
106
+ date: item.date!,
107
+ version: item.version,
108
+ type: item.type
109
+ }))
110
+
111
+ // 2. Timeline events from legacyStats.timeline (parsed from context.jsonl)
112
+ const timeline = stats.legacyStats?.timeline ?? []
113
+ const weekEvents = timeline.filter(e => {
114
+ if (!e.ts) return false
115
+ return isDateInWeek(e.ts, start, end)
116
+ })
117
+
118
+ // Count tasks completed
119
+ const tasksCompleted = weekEvents.filter(e =>
120
+ e.type === 'task_complete' ||
121
+ e.type === 'task_completed' ||
122
+ e.type === 'session_completed'
123
+ ).length
124
+
125
+ // Count bugs fixed
126
+ const bugsFixed = weekEvents.filter(e =>
127
+ e.type === 'bug_fix' ||
128
+ e.type === 'bug_reported'
129
+ ).length
130
+
131
+ // Count syncs
132
+ const syncs = weekEvents.filter(e => e.type === 'sync').length
133
+
134
+ // 3. Also aggregate from sessions
135
+ const sessions = stats.legacyStats?.sessions ?? []
136
+ let sessionTasksCompleted = 0
137
+ let sessionFeatures = 0
138
+ const activeDaysSet = new Set<string>()
139
+
140
+ for (const session of sessions) {
141
+ if (session.date && isDateInWeek(session.date, start, end)) {
142
+ activeDaysSet.add(session.date)
143
+ sessionTasksCompleted += session.tasksCompleted || 0
144
+ sessionFeatures += session.featuresShipped || 0
145
+ }
146
+ }
147
+
148
+ // Add active days from timeline
149
+ for (const e of weekEvents) {
150
+ if (e.ts) {
151
+ activeDaysSet.add(e.ts.split('T')[0])
152
+ }
153
+ }
154
+
155
+ return {
156
+ year,
157
+ week,
158
+ startDate: start,
159
+ endDate: end,
160
+ shipped,
161
+ tasksCompleted: tasksCompleted + sessionTasksCompleted,
162
+ bugsFixed,
163
+ syncs,
164
+ activeDays: activeDaysSet.size
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Generate plain text report for WhatsApp/email - client friendly
170
+ */
171
+ export function generateReportText(
172
+ weekData: WeekData | WeekData[],
173
+ projectName: string
174
+ ): string {
175
+ const weeks = Array.isArray(weekData) ? weekData : [weekData]
176
+
177
+ if (weeks.length === 0) {
178
+ return 'No weeks selected'
179
+ }
180
+
181
+ const lines: string[] = []
182
+
183
+ // Header - clean and professional
184
+ lines.push(`*${projectName}*`)
185
+
186
+ // Date range - human readable
187
+ if (weeks.length === 1) {
188
+ const w = weeks[0]
189
+ lines.push(`Weekly Report: ${formatDateRange(w.startDate, w.endDate)}`)
190
+ } else {
191
+ const firstWeek = weeks[0]
192
+ const lastWeek = weeks[weeks.length - 1]
193
+ lines.push(`Report: ${formatDateRange(firstWeek.startDate, lastWeek.endDate)}`)
194
+ }
195
+ lines.push('')
196
+
197
+ // Aggregate data
198
+ const allShipped = weeks.flatMap(w => w.shipped)
199
+ // Deduplicate by name, keeping first occurrence (with version info)
200
+ const uniqueShipsMap = new Map<string, ShippedItem>()
201
+ for (const ship of allShipped) {
202
+ if (!uniqueShipsMap.has(ship.name)) {
203
+ uniqueShipsMap.set(ship.name, ship)
204
+ }
205
+ }
206
+ // Sort by date descending (most recent first)
207
+ const uniqueShips = Array.from(uniqueShipsMap.values())
208
+ .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
209
+
210
+ // Group by date
211
+ const shipsByDate = new Map<string, ShippedItem[]>()
212
+ for (const ship of uniqueShips) {
213
+ if (!shipsByDate.has(ship.date)) {
214
+ shipsByDate.set(ship.date, [])
215
+ }
216
+ shipsByDate.get(ship.date)!.push(ship)
217
+ }
218
+
219
+ // What we delivered - grouped by date
220
+ if (uniqueShips.length > 0) {
221
+ lines.push('*Shipped:*')
222
+ for (const [date, ships] of shipsByDate.entries()) {
223
+ const dateStr = new Date(date).toLocaleDateString('en-US', {
224
+ weekday: 'short',
225
+ day: 'numeric',
226
+ month: 'short'
227
+ })
228
+ lines.push(`_${dateStr}_`)
229
+ for (const ship of ships) {
230
+ const versionStr = ship.version ? ` (${ship.version})` : ''
231
+ lines.push(`• ${ship.name}${versionStr}`)
232
+ }
233
+ }
234
+ lines.push('')
235
+ }
236
+
237
+ // Aggregate progress metrics
238
+ const totalTasks = weeks.reduce((sum, w) => sum + w.tasksCompleted, 0)
239
+ const totalBugs = weeks.reduce((sum, w) => sum + w.bugsFixed, 0)
240
+ const totalDays = weeks.reduce((sum, w) => sum + w.activeDays, 0)
241
+
242
+ // Progress section
243
+ if (totalTasks > 0 || totalBugs > 0 || totalDays > 0) {
244
+ lines.push('*Progress:*')
245
+ if (totalTasks > 0) {
246
+ lines.push(`• ${totalTasks} task${totalTasks !== 1 ? 's' : ''} completed`)
247
+ }
248
+ if (totalBugs > 0) {
249
+ lines.push(`• ${totalBugs} bug${totalBugs !== 1 ? 's' : ''} fixed`)
250
+ }
251
+ if (totalDays > 0) {
252
+ lines.push(`• ${totalDays} active day${totalDays !== 1 ? 's' : ''}`)
253
+ }
254
+ lines.push('')
255
+ }
256
+
257
+ // If nothing happened, be honest
258
+ if (uniqueShips.length === 0 && totalTasks === 0 && totalBugs === 0) {
259
+ lines.push('_In progress, no deliveries this week._')
260
+ lines.push('')
261
+ }
262
+
263
+ // Optional: Next steps placeholder
264
+ lines.push('*Next:*')
265
+ lines.push('• [To be defined]')
266
+
267
+ return lines.join('\n')
268
+ }
269
+
270
+ /**
271
+ * Get activity level for a week (for calendar indicator)
272
+ */
273
+ export function getWeekActivityLevel(
274
+ stats: StatsResult,
275
+ year: number,
276
+ week: number
277
+ ): 'none' | 'low' | 'medium' | 'high' {
278
+ const data = filterDataByWeek(stats, year, week)
279
+ const total = data.shipped.length + data.tasksCompleted + data.bugsFixed + data.syncs
280
+
281
+ if (total === 0) return 'none'
282
+ if (total <= 3) return 'low'
283
+ if (total <= 10) return 'medium'
284
+ return 'high'
285
+ }
@@ -463,68 +463,69 @@ export function parseShipped(content: string): ShippedFeature[] {
463
463
  const features: ShippedFeature[] = []
464
464
  if (!content) return features
465
465
 
466
- // Split by version headers: ## v1.2.3 - Name or ## 2025-01-01
467
- const sections = content.split(/^##\s+/m).filter(s => s.trim())
468
-
469
- for (const section of sections) {
470
- const lines = section.split('\n')
471
- const headerLine = lines[0]
472
-
473
- // Parse header: v1.2.3 - Feature Name or date
474
- const versionMatch = headerLine.match(/^(v[\d.]+)\s*[-–]\s*(.+)$/i)
475
- const dateMatch = headerLine.match(/^(\d{4}-\d{2}-\d{2})/)
476
-
477
- if (!versionMatch && !dateMatch) continue
478
-
479
- const feature: ShippedFeature = {
480
- date: dateMatch ? dateMatch[1] : new Date().toISOString().split('T')[0],
481
- name: versionMatch ? versionMatch[2].trim() : '',
482
- version: versionMatch ? versionMatch[1] : undefined
466
+ // Try ISO date format first: ## 2025-01-01
467
+ const isoDateSections = content.split(/^##\s+(\d{4}-\d{2}-\d{2})/m)
468
+
469
+ if (isoDateSections.length > 1) {
470
+ // Process pairs: [before, date1, content1, date2, content2, ...]
471
+ for (let i = 1; i < isoDateSections.length; i += 2) {
472
+ const sectionDate = isoDateSections[i]
473
+ const sectionContent = isoDateSections[i + 1] || ''
474
+ parseShippedSection(sectionContent, sectionDate, features)
483
475
  }
484
-
485
- // Parse feature metadata
486
- for (const line of lines.slice(1)) {
487
- const typeMatch = line.match(/Type\*?\*?:\s*(\w+)/i)
488
- const agentMatch = line.match(/Agent\*?\*?:\s*(\w+)/i)
489
- const timeMatch = line.match(/Time\*?\*?:\s*([^\n]+)/i)
490
- const commitMatch = line.match(/Commit\*?\*?:\s*(\w+)/i)
491
- const impactMatch = line.match(/Impact\*?\*?:\s*(\w+)/i)
492
- const filesMatch = line.match(/Files changed\*?\*?:\s*(\d+)/i)
493
- const addedMatch = line.match(/\+(\d+)/i)
494
- const removedMatch = line.match(/-(\d+)/i)
495
-
496
- if (typeMatch) feature.type = typeMatch[1]
497
- if (agentMatch) feature.agent = agentMatch[1]
498
- if (timeMatch) feature.time = timeMatch[1].trim()
499
- if (commitMatch) feature.commit = commitMatch[1]
500
- if (impactMatch) feature.impact = impactMatch[1]
501
- if (filesMatch) feature.filesChanged = parseInt(filesMatch[1])
502
- if (addedMatch) feature.linesAdded = parseInt(addedMatch[1])
503
- if (removedMatch) feature.linesRemoved = parseInt(removedMatch[1])
504
-
505
- // Root cause and solution for fixes
506
- if (line.includes('Root Cause')) {
507
- feature.rootCause = line.replace(/.*Root Cause\*?\*?:\s*/i, '').trim()
508
- }
509
- if (line.includes('Solution')) {
510
- feature.solution = line.replace(/.*Solution\*?\*?:\s*/i, '').trim()
476
+ } else {
477
+ // Try month format: ## November 2025 or ## December 2024
478
+ const monthDateSections = content.split(/^##\s+(\w+\s+\d{4})/m)
479
+
480
+ if (monthDateSections.length > 1) {
481
+ for (let i = 1; i < monthDateSections.length; i += 2) {
482
+ const sectionDate = monthDateSections[i]
483
+ const sectionContent = monthDateSections[i + 1] || ''
484
+ parseShippedSection(sectionContent, sectionDate, features)
511
485
  }
486
+ } else {
487
+ // No date headers - parse entire content with inline dates
488
+ parseShippedSection(content, '', features)
512
489
  }
490
+ }
513
491
 
514
- // If we found name from features list
515
- if (!feature.name) {
516
- const featureLineMatch = section.match(/\*\*([^*]+)\*\*/m)
517
- if (featureLineMatch) {
518
- feature.name = featureLineMatch[1].trim()
519
- }
520
- }
492
+ return features.slice(0, 30) // Last 30
493
+ }
521
494
 
522
- if (feature.name) {
523
- features.push(feature)
524
- }
495
+ function parseShippedSection(sectionContent: string, sectionDate: string, features: ShippedFeature[]) {
496
+ // Parse bullet points: - **Name** (version) or - **Name** - 2025-01-01
497
+ const bulletRegex = /^-\s+\*\*(.+?)\*\*(?:\s*\(([^)]+)\))?(?:\s*-\s*(\d{4}-\d{2}-\d{2}))?/gm
498
+ let match
499
+ while ((match = bulletRegex.exec(sectionContent)) !== null) {
500
+ const name = match[1].trim()
501
+ const versionOrNote = match[2]?.trim()
502
+ const inlineDate = match[3]
503
+
504
+ // Extract version if it starts with 'v'
505
+ const version = versionOrNote?.match(/^v[\d.]+/) ? versionOrNote : undefined
506
+
507
+ features.push({
508
+ date: inlineDate || sectionDate || new Date().toISOString().split('T')[0],
509
+ name,
510
+ version
511
+ })
525
512
  }
526
513
 
527
- return features.slice(0, 20) // Last 20
514
+ // Also check for ### Feature Name headers (alternative format)
515
+ const headerRegex = /^###\s+(.+?)(?:\s+(v[\d.]+))?\s*$/gm
516
+ while ((match = headerRegex.exec(sectionContent)) !== null) {
517
+ const name = match[1].trim()
518
+ const version = match[2]
519
+
520
+ // Avoid duplicates if same feature in both formats
521
+ if (!features.some(f => f.date === sectionDate && f.name === name)) {
522
+ features.push({
523
+ date: sectionDate || new Date().toISOString().split('T')[0],
524
+ name,
525
+ version
526
+ })
527
+ }
528
+ }
528
529
  }
529
530
 
530
531
  // Parse ideas.md - Full idea structure
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Project Colors - Consistent color generation based on projectId
3
+ *
4
+ * Uses a hash of the projectId to generate a consistent color
5
+ * that matches between UI elements and browser tab titles.
6
+ */
7
+
8
+ // Color palette - visually distinct colors that work well in both
9
+ // Tailwind classes and as emojis
10
+ export const PROJECT_COLORS = [
11
+ { name: 'red', emoji: '🔴', bg: 'bg-red-500', text: 'text-red-500' },
12
+ { name: 'orange', emoji: '🟠', bg: 'bg-orange-500', text: 'text-orange-500' },
13
+ { name: 'yellow', emoji: '🟡', bg: 'bg-yellow-500', text: 'text-yellow-500' },
14
+ { name: 'green', emoji: '🟢', bg: 'bg-green-500', text: 'text-green-500' },
15
+ { name: 'blue', emoji: '🔵', bg: 'bg-blue-500', text: 'text-blue-500' },
16
+ { name: 'purple', emoji: '🟣', bg: 'bg-purple-500', text: 'text-purple-500' },
17
+ { name: 'brown', emoji: '🟤', bg: 'bg-amber-700', text: 'text-amber-700' },
18
+ ] as const
19
+
20
+ export type ProjectColor = typeof PROJECT_COLORS[number]
21
+
22
+ /**
23
+ * Simple hash function for strings
24
+ */
25
+ function hashString(str: string): number {
26
+ let hash = 0
27
+ for (let i = 0; i < str.length; i++) {
28
+ const char = str.charCodeAt(i)
29
+ hash = ((hash << 5) - hash) + char
30
+ hash = hash & hash // Convert to 32bit integer
31
+ }
32
+ return Math.abs(hash)
33
+ }
34
+
35
+ /**
36
+ * Get a consistent color for a project based on its ID
37
+ */
38
+ export function getProjectColor(projectId: string): ProjectColor {
39
+ const hash = hashString(projectId)
40
+ const index = hash % PROJECT_COLORS.length
41
+ return PROJECT_COLORS[index]
42
+ }
43
+
44
+ /**
45
+ * Get just the emoji for a project (for use in titles)
46
+ * Returns a neutral symbol instead of colored circles
47
+ */
48
+ export function getProjectEmoji(projectId: string): string {
49
+ return '▸'
50
+ }
51
+
52
+ /**
53
+ * Get the Tailwind background class for a project
54
+ * Returns neutral color - projects no longer have color-coded backgrounds
55
+ */
56
+ export function getProjectBgClass(projectId: string): string {
57
+ return 'bg-muted'
58
+ }
@@ -7,6 +7,7 @@ import { join, dirname } from 'path'
7
7
  import { homedir } from 'os'
8
8
  import { exec } from 'child_process'
9
9
  import { promisify } from 'util'
10
+ import { listSessions } from './pty'
10
11
 
11
12
  const execAsync = promisify(exec)
12
13
 
@@ -154,11 +155,16 @@ export async function getProjects() {
154
155
  let hasActiveSession = false
155
156
  let lastActivity: string | null = null
156
157
 
157
- // Try current session first
158
+ // Check for real PTY sessions (actual Claude sessions in memory)
159
+ try {
160
+ const activeSessions = listSessions()
161
+ hasActiveSession = activeSessions.some(s => s.projectDir === repoPath)
162
+ } catch {}
163
+
164
+ // Try current session for lastActivity only
158
165
  try {
159
166
  const sessionPath = join(storagePath, 'sessions', 'current.json')
160
167
  const sessionData = JSON.parse(await fs.readFile(sessionPath, 'utf-8'))
161
- hasActiveSession = sessionData.status === 'active'
162
168
  lastActivity = sessionData.startedAt || sessionData.updatedAt
163
169
  } catch {}
164
170
 
@@ -188,9 +194,10 @@ export async function getProjects() {
188
194
  }
189
195
  }
190
196
 
191
- // Count ideas and next tasks
197
+ // Count ideas, next tasks, and shipped items
192
198
  let ideasCount = 0
193
199
  let nextTasksCount = 0
200
+ let shippedCount = 0
194
201
  try {
195
202
  const ideasContent = await fs.readFile(join(storagePath, 'planning', 'ideas.md'), 'utf-8')
196
203
  ideasCount = (ideasContent.match(/^- /gm) || []).length
@@ -199,6 +206,13 @@ export async function getProjects() {
199
206
  const nextContent = await fs.readFile(join(storagePath, 'core', 'next.md'), 'utf-8')
200
207
  nextTasksCount = (nextContent.match(/^- /gm) || []).length
201
208
  } catch {}
209
+ try {
210
+ const shippedContent = await fs.readFile(join(storagePath, 'progress', 'shipped.md'), 'utf-8')
211
+ // Count shipped items: either "- **Name**" or "### Name" format
212
+ const bulletItems = (shippedContent.match(/^- \*\*/gm) || []).length
213
+ const headingItems = (shippedContent.match(/^### /gm) || []).length
214
+ shippedCount = bulletItems + headingItems
215
+ } catch {}
202
216
 
203
217
  // Find favicon/icon in project repo
204
218
  let iconPath: string | null = null
@@ -237,6 +251,7 @@ export async function getProjects() {
237
251
  lastActivity,
238
252
  ideasCount,
239
253
  nextTasksCount,
254
+ shippedCount,
240
255
  techStack,
241
256
  iconPath
242
257
  })
@@ -318,6 +333,23 @@ export async function getProject(projectId: string) {
318
333
  currentTask = await fs.readFile(nowPath, 'utf-8')
319
334
  } catch {}
320
335
 
336
+ // Count shipped and queue items (DRY - same logic as getProjects)
337
+ let shippedCount = 0
338
+ let nextTasksCount = 0
339
+ try {
340
+ const shippedContent = await fs.readFile(join(storagePath, 'progress', 'shipped.md'), 'utf-8')
341
+ const bulletItems = (shippedContent.match(/^- \*\*/gm) || []).length
342
+ const headingItems = (shippedContent.match(/^### /gm) || []).length
343
+ shippedCount = bulletItems + headingItems
344
+ } catch {}
345
+ try {
346
+ const nextContent = await fs.readFile(join(storagePath, 'core', 'next.md'), 'utf-8')
347
+ nextTasksCount = (nextContent.match(/^- /gm) || []).length
348
+ } catch {}
349
+
350
+ const total = shippedCount + nextTasksCount
351
+ const completionRate = total > 0 ? Math.round((shippedCount / total) * 100) : 0
352
+
321
353
  // Fallback: Extract stats from claudeMd Quick Reference table if not from project.json
322
354
  if (!version) {
323
355
  const versionMatch = claudeMd.match(/\*\*Version\*\*\s*\|\s*([^\n|]+)/)
@@ -380,7 +412,11 @@ export async function getProject(projectId: string) {
380
412
  filesCount,
381
413
  commitsCount,
382
414
  techStack,
383
- iconPath
415
+ iconPath,
416
+ // Counts (DRY - same source as dashboard)
417
+ shippedCount,
418
+ nextTasksCount,
419
+ completionRate
384
420
  }
385
421
  } catch {
386
422
  return null
@@ -424,11 +460,28 @@ export async function getProjectStatus(projectId: string) {
424
460
  const projectPath = join(GLOBAL_STORAGE, projectId)
425
461
 
426
462
  let session = null
463
+ let repoPath: string | null = null
427
464
  try {
428
465
  const sessionPath = join(projectPath, 'sessions', 'current.json')
429
466
  session = JSON.parse(await fs.readFile(sessionPath, 'utf-8'))
430
467
  } catch {}
431
468
 
469
+ // Get repoPath from project.json for PTY session check
470
+ try {
471
+ const projectJsonPath = join(projectPath, 'project.json')
472
+ const projectJson = JSON.parse(await fs.readFile(projectJsonPath, 'utf-8'))
473
+ repoPath = projectJson.repoPath || null
474
+ } catch {}
475
+
476
+ // Check for real PTY sessions
477
+ let hasActiveSession = false
478
+ if (repoPath) {
479
+ try {
480
+ const activeSessions = listSessions()
481
+ hasActiveSession = activeSessions.some(s => s.projectDir === repoPath)
482
+ } catch {}
483
+ }
484
+
432
485
  let ideas: string[] = []
433
486
  try {
434
487
  const ideasPath = join(projectPath, 'planning', 'ideas.md')
@@ -446,7 +499,7 @@ export async function getProjectStatus(projectId: string) {
446
499
  return {
447
500
  projectId,
448
501
  session,
449
- hasActiveSession: session?.status === 'active',
502
+ hasActiveSession,
450
503
  ideas,
451
504
  nextTasks
452
505
  }
@@ -19,6 +19,11 @@ export interface ProjectJson {
19
19
  commitCount: number
20
20
  createdAt: string
21
21
  lastSync: string
22
+ version?: string | null
23
+ // Counts - DRY: single source of truth for dashboard/detail
24
+ shippedCount: number
25
+ nextTasksCount: number
26
+ completionRate: number
22
27
  }
23
28
 
24
29
  /**
@@ -38,7 +43,12 @@ export const getProject = cache(async (projectId: string): Promise<ProjectJson |
38
43
  fileCount: project.filesCount ? parseInt(project.filesCount) : 0,
39
44
  commitCount: project.commitsCount ? parseInt(project.commitsCount) : 0,
40
45
  createdAt: new Date().toISOString(),
41
- lastSync: new Date().toISOString()
46
+ lastSync: new Date().toISOString(),
47
+ version: project.version || null,
48
+ // DRY: Pass through counts from lib/projects.ts (single source of truth)
49
+ shippedCount: project.shippedCount ?? 0,
50
+ nextTasksCount: project.nextTasksCount ?? 0,
51
+ completionRate: project.completionRate ?? 0
42
52
  }
43
53
  }
44
54
  } catch {
@@ -1,6 +1,6 @@
1
1
  /// <reference types="next" />
2
2
  /// <reference types="next/image-types/global" />
3
- import "./.next/dev/types/routes.d.ts";
3
+ import "./.next/types/routes.d.ts";
4
4
 
5
5
  // NOTE: This file should not be edited
6
6
  // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -5,7 +5,7 @@
5
5
  "scripts": {
6
6
  "dev": "NODE_ENV=development PORT=9471 bun server.ts",
7
7
  "build": "bun next build",
8
- "start": "NODE_ENV=production PORT=9472 bun server.ts",
8
+ "start": "NODE_ENV=production PORT=9472 node --import tsx server.ts",
9
9
  "lint": "eslint"
10
10
  },
11
11
  "dependencies": {
@@ -13,6 +13,7 @@
13
13
  "@radix-ui/react-dialog": "^1.1.15",
14
14
  "@radix-ui/react-dropdown-menu": "^2.1.16",
15
15
  "@radix-ui/react-scroll-area": "^1.2.10",
16
+ "@radix-ui/react-select": "^2.2.6",
16
17
  "@radix-ui/react-slot": "^1.2.4",
17
18
  "@radix-ui/react-tabs": "^1.1.13",
18
19
  "@radix-ui/react-tooltip": "^1.2.8",
@@ -31,15 +32,18 @@
31
32
  "react": "19.2.0",
32
33
  "react-dom": "19.2.0",
33
34
  "react-markdown": "^10.1.0",
35
+ "react-resizable-panels": "^3.0.6",
34
36
  "recharts": "^3.5.1",
35
37
  "remark-gfm": "^4.0.1",
36
38
  "tailwind-merge": "^3.4.0",
39
+ "vaul": "^1.1.2",
37
40
  "ws": "^8.18.3",
38
41
  "zod": "^4.1.13"
39
42
  },
40
43
  "devDependencies": {
41
44
  "@tailwindcss/postcss": "^4",
42
45
  "@types/bun": "latest",
46
+ "tsx": "^4.21.0",
43
47
  "@types/node": "^20",
44
48
  "@types/react": "^19",
45
49
  "@types/react-dom": "^19",