prjct-cli 0.13.2 → 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
@@ -0,0 +1,59 @@
1
+ import { notFound } from 'next/navigation'
2
+ import Link from 'next/link'
3
+ import type { Metadata } from 'next'
4
+ import { ArrowLeft } from 'lucide-react'
5
+ import { getStats } from '@/lib/services/stats.server'
6
+ import { getProject } from '@/lib/services/projects.server'
7
+ import { getProjectEmoji } from '@/lib/project-colors'
8
+ import { WeeklyReports } from '@/components/WeeklyReports'
9
+
10
+ export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
11
+ const { id: projectId } = await params
12
+ const project = await getProject(projectId)
13
+ const projectName = project?.name ?? projectId
14
+ const emoji = getProjectEmoji(projectId)
15
+
16
+ return {
17
+ title: `${emoji} ${projectName} / Reports / p.`,
18
+ }
19
+ }
20
+
21
+ interface PageProps {
22
+ params: Promise<{ id: string }>
23
+ }
24
+
25
+ export default async function ReportsPage({ params }: PageProps) {
26
+ const { id: projectId } = await params
27
+
28
+ const [project, stats] = await Promise.all([
29
+ getProject(projectId),
30
+ getStats(projectId)
31
+ ])
32
+
33
+ if (!stats.hasData) {
34
+ notFound()
35
+ }
36
+
37
+ const projectName = project?.name ?? projectId
38
+
39
+ return (
40
+ <div className="flex h-full flex-col p-4 md:p-6 overflow-auto">
41
+ {/* Header with back navigation */}
42
+ <div className="pl-10 md:pl-0 mb-6">
43
+ <Link
44
+ href={`/project/${projectId}`}
45
+ className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors mb-4"
46
+ >
47
+ <ArrowLeft className="h-4 w-4" />
48
+ Back to {projectName}
49
+ </Link>
50
+ <h1 className="text-3xl font-bold">Weekly Reports</h1>
51
+ </div>
52
+
53
+ <div className="pl-10 md:pl-0">
54
+ <WeeklyReports stats={stats} projectName={projectName} />
55
+ </div>
56
+ <div className="h-4" />
57
+ </div>
58
+ )
59
+ }
@@ -0,0 +1,58 @@
1
+ import type { Metadata } from 'next'
2
+ import { getStats } from '@/lib/services/stats.server'
3
+ import { getProject } from '@/lib/projects'
4
+ import { getProjectEmoji } from '@/lib/project-colors'
5
+ import { PrintableReport } from '@/components/WeeklyReports/PrintableReport'
6
+
7
+ export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
8
+ const { id: projectId } = await params
9
+ const project = await getProject(projectId)
10
+ const projectName = project?.name ?? projectId
11
+ const emoji = getProjectEmoji(projectId)
12
+
13
+ return {
14
+ title: `${emoji} ${projectName} / Print / p.`,
15
+ }
16
+ }
17
+
18
+ interface Props {
19
+ params: Promise<{ id: string }>
20
+ searchParams: Promise<{ weeks?: string; year?: string }>
21
+ }
22
+
23
+ export default async function PrintReportPage({ params, searchParams }: Props) {
24
+ const { id } = await params
25
+ const { weeks, year } = await searchParams
26
+
27
+ const project = await getProject(id)
28
+ if (!project) {
29
+ return <div className="p-8 text-center">Project not found</div>
30
+ }
31
+
32
+ const stats = await getStats(id)
33
+
34
+ // Parse weeks from query params (comma-separated)
35
+ const selectedWeeks = weeks
36
+ ? weeks.split(',').map(Number).filter(n => !isNaN(n))
37
+ : [getCurrentWeek()]
38
+
39
+ const selectedYear = year ? parseInt(year) : new Date().getFullYear()
40
+
41
+ return (
42
+ <PrintableReport
43
+ stats={stats}
44
+ projectName={project.name}
45
+ selectedWeeks={selectedWeeks}
46
+ year={selectedYear}
47
+ />
48
+ )
49
+ }
50
+
51
+ function getCurrentWeek(): number {
52
+ const now = new Date()
53
+ const d = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()))
54
+ const dayNum = d.getUTCDay() || 7
55
+ d.setUTCDate(d.getUTCDate() + 4 - dayNum)
56
+ const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
57
+ return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7)
58
+ }
@@ -16,7 +16,6 @@ export function ActivityTimeline({ timeline, className }: ActivityTimelineProps)
16
16
 
17
17
  return (
18
18
  <BentoCard
19
- size="full"
20
19
  title="Recent Activity"
21
20
  icon={Activity}
22
21
  count={timeline.length > 0 ? `${timeline.length} events` : undefined}
@@ -1,63 +1,93 @@
1
- import { BentoCard } from '@/components/BentoCard'
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
2
4
  import { EmptyState } from '@/components/EmptyState'
3
- import { Bot, Star, TrendingUp } from 'lucide-react'
5
+ import { ExpandButton } from '@/components/ExpandButton'
6
+ import { Bot, Star, TrendingUp, Wrench } from 'lucide-react'
4
7
  import { Badge } from '@/components/ui/badge'
5
8
  import { cn } from '@/lib/utils'
6
9
  import type { AgentsCardProps } from './AgentsCard.types'
7
10
 
8
- export function AgentsCard({ agents, className }: AgentsCardProps) {
9
- const displayAgents = agents.slice(0, 8)
11
+ const COLLAPSED_LIMIT = 12
12
+
13
+ export function AgentsCard({ agents, codeHref, className }: AgentsCardProps) {
14
+ const [expanded, setExpanded] = useState(false)
15
+ const displayAgents = expanded ? agents : agents.slice(0, COLLAPSED_LIMIT)
16
+ const hasMore = agents.length > COLLAPSED_LIMIT
10
17
 
11
18
  return (
12
- <BentoCard
13
- size="1x1"
14
- title="Agents"
15
- icon={Bot}
16
- count={agents.length}
17
- className={className}
18
- >
19
+ <div className={cn(
20
+ 'relative overflow-hidden rounded-xl border bg-card p-4',
21
+ className
22
+ )}>
23
+ <div className="flex items-center justify-between mb-3">
24
+ <div className="flex items-center gap-2">
25
+ <Bot className="h-4 w-4 text-muted-foreground" />
26
+ <span className="text-xs font-bold uppercase tracking-[0.15em] text-muted-foreground">
27
+ Agents
28
+ </span>
29
+ </div>
30
+ <span className="text-xs font-medium text-muted-foreground tabular-nums">
31
+ {agents.length} available
32
+ </span>
33
+ </div>
34
+
19
35
  {agents.length === 0 ? (
20
36
  <EmptyState
21
37
  icon={Bot}
22
38
  title="No agents"
23
39
  description="Run /p:sync to generate"
40
+ command="/p:sync"
41
+ href={codeHref}
24
42
  compact
25
43
  />
26
44
  ) : (
27
- <div className="flex flex-wrap gap-1.5">
45
+ <div className="space-y-2">
28
46
  {displayAgents.map((agent) => {
29
47
  const hasPerformance = agent.successRate !== undefined
30
48
  const isTopPerformer = hasPerformance && agent.successRate! >= 80
31
49
 
32
50
  return (
33
- <Badge
51
+ <div
34
52
  key={agent.name}
35
- variant="secondary"
36
- className={cn(
37
- "text-xs px-2 py-0.5 font-mono inline-flex items-center gap-1",
38
- isTopPerformer && "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20"
39
- )}
53
+ className="flex items-center justify-between gap-2 py-1.5 px-2 rounded-lg -mx-2 hover:bg-muted/50"
40
54
  >
41
- {isTopPerformer && <Star className="h-2.5 w-2.5" />}
42
- @{agent.name}
43
- {hasPerformance && (
44
- <span className="text-[9px] opacity-70">
45
- {agent.successRate}%
46
- </span>
47
- )}
48
- {agent.improving && (
49
- <TrendingUp className="h-2.5 w-2.5 text-emerald-500" />
50
- )}
51
- </Badge>
55
+ <div className="flex items-center gap-2 min-w-0">
56
+ {isTopPerformer ? (
57
+ <Star className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
58
+ ) : (
59
+ <Wrench className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
60
+ )}
61
+ <span className="font-mono text-sm truncate">@{agent.name}</span>
62
+ </div>
63
+ <div className="flex items-center gap-2 shrink-0">
64
+ {hasPerformance && (
65
+ <span className="text-xs font-medium tabular-nums text-muted-foreground">
66
+ {agent.successRate}%
67
+ </span>
68
+ )}
69
+ {agent.improving && (
70
+ <TrendingUp className="h-3 w-3 text-muted-foreground" />
71
+ )}
72
+ {agent.tasksCompleted !== undefined && agent.tasksCompleted > 0 && (
73
+ <Badge variant="secondary" className="text-xs px-1.5 py-0 h-4">
74
+ {agent.tasksCompleted} tasks
75
+ </Badge>
76
+ )}
77
+ </div>
78
+ </div>
52
79
  )
53
80
  })}
54
- {agents.length > 8 && (
55
- <Badge variant="outline" className="text-xs px-2 py-0.5">
56
- +{agents.length - 8}
57
- </Badge>
81
+ {hasMore && (
82
+ <ExpandButton
83
+ expanded={expanded}
84
+ totalCount={agents.length}
85
+ collapsedLimit={COLLAPSED_LIMIT}
86
+ onToggle={() => setExpanded(!expanded)}
87
+ />
58
88
  )}
59
89
  </div>
60
90
  )}
61
- </BentoCard>
91
+ </div>
62
92
  )
63
93
  }
@@ -9,5 +9,6 @@ export interface Agent {
9
9
 
10
10
  export interface AgentsCardProps {
11
11
  agents: Agent[]
12
+ codeHref?: string
12
13
  className?: string
13
14
  }
@@ -8,7 +8,11 @@ import {
8
8
  HelpCircle,
9
9
  Menu,
10
10
  PanelLeft,
11
+ Terminal,
11
12
  } from 'lucide-react'
13
+ import { useGlobalTerminal } from '@/context/GlobalTerminalContext'
14
+ import { Badge } from '@/components/ui/badge'
15
+ import { ProjectSelectorModal } from '@/components/ProjectSelectorModal'
12
16
  import { Logo } from '@/components/Logo'
13
17
  import { cn } from '@/lib/utils'
14
18
  import { useState, useEffect } from 'react'
@@ -22,11 +26,15 @@ const navItems = [
22
26
  function SidebarContent({
23
27
  onNavigate,
24
28
  isCollapsed = false,
25
- onToggleCollapse
29
+ onToggleCollapse,
30
+ onTerminalClick,
31
+ sessionCount
26
32
  }: {
27
33
  onNavigate?: () => void
28
34
  isCollapsed?: boolean
29
35
  onToggleCollapse?: () => void
36
+ onTerminalClick?: () => void
37
+ sessionCount?: number
30
38
  }) {
31
39
  const pathname = usePathname()
32
40
 
@@ -94,6 +102,45 @@ function SidebarContent({
94
102
  }
95
103
  return linkContent
96
104
  })}
105
+
106
+ {/* Terminal Button */}
107
+ {onTerminalClick && (
108
+ (() => {
109
+ const terminalButton = (
110
+ <button
111
+ onClick={onTerminalClick}
112
+ className={cn(
113
+ 'flex items-center rounded-md transition-colors min-h-[44px] w-full',
114
+ isCollapsed ? 'justify-center px-2' : 'gap-3 px-3',
115
+ 'py-2.5',
116
+ 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
117
+ )}
118
+ >
119
+ <div className="relative">
120
+ <Terminal className="h-5 w-5 shrink-0" />
121
+ {sessionCount !== undefined && sessionCount > 0 && (
122
+ <Badge
123
+ className="absolute -top-2 -right-2 h-4 w-4 p-0 flex items-center justify-center text-[10px] bg-primary"
124
+ >
125
+ {sessionCount}
126
+ </Badge>
127
+ )}
128
+ </div>
129
+ {!isCollapsed && <span className="text-sm font-medium">Terminal</span>}
130
+ </button>
131
+ )
132
+
133
+ if (isCollapsed) {
134
+ return (
135
+ <Tooltip key="terminal">
136
+ <TooltipTrigger asChild>{terminalButton}</TooltipTrigger>
137
+ <TooltipContent side="right">Terminal</TooltipContent>
138
+ </Tooltip>
139
+ )
140
+ }
141
+ return terminalButton
142
+ })()
143
+ )}
97
144
  </div>
98
145
  </nav>
99
146
 
@@ -138,10 +185,66 @@ function SidebarContent({
138
185
  )
139
186
  }
140
187
 
188
+ const SIDEBAR_COLLAPSED_KEY = 'prjct-sidebar-collapsed'
189
+
141
190
  export function AppSidebar() {
142
191
  const [isOpen, setIsOpen] = useState(false)
143
192
  const [isMobile, setIsMobile] = useState(false)
144
193
  const [isCollapsed, setIsCollapsed] = useState(false)
194
+ const [showProjectSelector, setShowProjectSelector] = useState(false)
195
+ const pathname = usePathname()
196
+
197
+ // Terminal context
198
+ const {
199
+ toggleDock,
200
+ openDock,
201
+ getTotalSessionCount,
202
+ projectSessions,
203
+ createSessionForProject,
204
+ } = useGlobalTerminal()
205
+
206
+ const sessionCount = getTotalSessionCount()
207
+
208
+ // Handle terminal button click
209
+ const handleTerminalClick = () => {
210
+ // If we're on a project page, auto-detect project
211
+ const projectMatch = pathname?.match(/\/project\/([^/]+)/)
212
+
213
+ if (projectMatch) {
214
+ const projectId = projectMatch[1]
215
+ const existingProject = projectSessions.get(projectId)
216
+
217
+ if (existingProject) {
218
+ // Project already has sessions, just toggle dock
219
+ toggleDock()
220
+ } else {
221
+ // Need to show project selector to get project details
222
+ // For now, open dock (will be empty) - the user should create session from the dock
223
+ setShowProjectSelector(true)
224
+ }
225
+ } else if (sessionCount > 0) {
226
+ // Has sessions, toggle dock
227
+ toggleDock()
228
+ } else {
229
+ // No sessions and no project context - show project selector
230
+ setShowProjectSelector(true)
231
+ }
232
+ }
233
+
234
+ // Load collapsed state from localStorage on mount
235
+ useEffect(() => {
236
+ const stored = localStorage.getItem(SIDEBAR_COLLAPSED_KEY)
237
+ if (stored !== null) {
238
+ setIsCollapsed(stored === 'true')
239
+ }
240
+ }, [])
241
+
242
+ // Persist collapsed state to localStorage
243
+ const handleToggleCollapse = () => {
244
+ const newValue = !isCollapsed
245
+ setIsCollapsed(newValue)
246
+ localStorage.setItem(SIDEBAR_COLLAPSED_KEY, String(newValue))
247
+ }
145
248
 
146
249
  // Detect mobile on mount and resize
147
250
  useEffect(() => {
@@ -166,7 +269,11 @@ export function AppSidebar() {
166
269
  </button>
167
270
  </SheetTrigger>
168
271
  <SheetContent side="left" className="w-[280px] p-0 flex flex-col">
169
- <SidebarContent onNavigate={() => setIsOpen(false)} />
272
+ <SidebarContent
273
+ onNavigate={() => setIsOpen(false)}
274
+ onTerminalClick={handleTerminalClick}
275
+ sessionCount={sessionCount}
276
+ />
170
277
  </SheetContent>
171
278
  </Sheet>
172
279
  )
@@ -174,15 +281,32 @@ export function AppSidebar() {
174
281
 
175
282
  // Desktop: Collapsible sidebar
176
283
  return (
177
- <aside className={cn(
178
- "hidden md:flex h-full flex-col border-r border-border bg-card transition-all duration-200",
179
- isCollapsed ? "w-14" : "w-60"
180
- )}>
181
- <SidebarContent
182
- isCollapsed={isCollapsed}
183
- onToggleCollapse={() => setIsCollapsed(!isCollapsed)}
184
- />
185
- </aside>
284
+ <>
285
+ <aside className={cn(
286
+ "hidden md:flex h-full flex-col border-r border-border bg-card transition-all duration-200",
287
+ isCollapsed ? "w-14" : "w-60"
288
+ )}>
289
+ <SidebarContent
290
+ isCollapsed={isCollapsed}
291
+ onToggleCollapse={handleToggleCollapse}
292
+ onTerminalClick={handleTerminalClick}
293
+ sessionCount={sessionCount}
294
+ />
295
+ </aside>
296
+
297
+ {/* Project Selector Modal - will be implemented separately */}
298
+ {showProjectSelector && (
299
+ <ProjectSelectorModal
300
+ isOpen={showProjectSelector}
301
+ onClose={() => setShowProjectSelector(false)}
302
+ onSelectProject={(projectId, projectName, projectPath) => {
303
+ createSessionForProject(projectId, projectName, projectPath)
304
+ openDock()
305
+ setShowProjectSelector(false)
306
+ }}
307
+ />
308
+ )}
309
+ </>
186
310
  )
187
311
  }
188
312
 
@@ -10,7 +10,7 @@ export const BENTO_SIZE_CLASSES: Record<BentoSize, string> = {
10
10
 
11
11
  export const ACCENT_STYLES: Record<AccentColor, string> = {
12
12
  default: '',
13
- success: 'border-emerald-500/20 bg-emerald-500/5',
14
- warning: 'border-amber-500/20 bg-amber-500/5',
15
- destructive: 'border-destructive/20 bg-destructive/5',
13
+ success: 'border-muted-foreground/20',
14
+ warning: 'border-muted-foreground/20',
15
+ destructive: 'border-muted-foreground/20',
16
16
  }
@@ -19,6 +19,7 @@ export function BentoCard({
19
19
  'p-3 sm:p-4',
20
20
  'hover:shadow-md hover:border-foreground/20',
21
21
  'active:scale-[0.99]',
22
+ 'min-w-0 max-w-full',
22
23
  BENTO_SIZE_CLASSES[size],
23
24
  ACCENT_STYLES[accentColor],
24
25
  className
@@ -29,7 +30,7 @@ export function BentoCard({
29
30
  <div className="flex items-center gap-2">
30
31
  {Icon && <Icon className="h-4 w-4 sm:h-3.5 sm:w-3.5 text-muted-foreground" />}
31
32
  {title && (
32
- <span className="text-[11px] sm:text-[10px] font-bold uppercase tracking-[0.15em] text-muted-foreground">
33
+ <span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
33
34
  {title}
34
35
  </span>
35
36
  )}
@@ -7,8 +7,8 @@ export function BentoGrid({ className, children }: BentoGridProps) {
7
7
  className={cn(
8
8
  'grid',
9
9
  'gap-3 sm:gap-4',
10
- 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-4',
11
- 'auto-rows-[minmax(120px,auto)] sm:auto-rows-[minmax(140px,auto)]',
10
+ 'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
11
+ 'auto-rows-[minmax(160px,auto)] sm:auto-rows-[minmax(180px,auto)]',
12
12
  className
13
13
  )}
14
14
  >
@@ -1,67 +1,75 @@
1
- import { BentoCard } from '@/components/BentoCard'
2
- import { AlertTriangle, Clock } from 'lucide-react'
1
+ 'use client'
2
+
3
+ import { useState } from 'react'
4
+ import { AlertTriangle, Clock, CheckCircle2 } from 'lucide-react'
5
+ import { ExpandButton } from '@/components/ExpandButton'
3
6
  import { cn } from '@/lib/utils'
4
7
  import type { BlockersCardProps } from './BlockersCard.types'
5
8
 
6
- export function BlockersCard({ blockers, className }: BlockersCardProps) {
9
+ const COLLAPSED_LIMIT = 5
10
+
11
+ export function BlockersCard({ blockers, codeHref, className }: BlockersCardProps) {
12
+ const [expanded, setExpanded] = useState(false)
7
13
  const hasBlockers = blockers.length > 0
14
+ const displayBlockers = expanded ? blockers : blockers.slice(0, COLLAPSED_LIMIT)
15
+ const hasMore = blockers.length > COLLAPSED_LIMIT
8
16
 
9
17
  return (
10
- <BentoCard
11
- size="1x1"
12
- title="Blockers"
13
- icon={AlertTriangle}
14
- className={cn(
15
- hasBlockers && 'border-amber-500/50 dark:border-amber-500/30',
16
- className
17
- )}
18
- >
19
- <div className="flex flex-col h-full">
20
- {!hasBlockers ? (
21
- <div className="flex flex-col items-center justify-center h-full text-center">
22
- <div className="text-2xl mb-1">&#x2705;</div>
23
- <p className="text-sm text-muted-foreground">No blockers</p>
24
- <p className="text-xs text-muted-foreground mt-1">Keep the momentum!</p>
25
- </div>
26
- ) : (
27
- <>
28
- <div className="flex items-center gap-2 mb-2">
29
- <span className="text-2xl font-bold text-amber-600 dark:text-amber-400">
30
- {blockers.length}
31
- </span>
32
- <span className="text-xs text-muted-foreground">
33
- {blockers.length === 1 ? 'task blocked' : 'tasks blocked'}
34
- </span>
35
- </div>
36
-
37
- <div className="flex-1 overflow-auto space-y-2">
38
- {blockers.slice(0, 3).map((blocker, i) => (
39
- <div
40
- key={i}
41
- className="p-2 rounded-md bg-amber-500/10 dark:bg-amber-500/5 border border-amber-500/20"
42
- >
43
- <p className="text-xs font-medium truncate">{blocker.task}</p>
44
- <p className="text-xs text-muted-foreground truncate mt-0.5">
45
- {blocker.reason}
46
- </p>
47
- <div className="flex items-center gap-1 mt-1">
48
- <Clock className="h-3 w-3 text-muted-foreground" />
49
- <span className="text-xs text-muted-foreground">
50
- {blocker.daysBlocked}d
51
- </span>
52
- </div>
53
- </div>
54
- ))}
55
- </div>
56
-
57
- {blockers.length > 3 && (
58
- <p className="text-xs text-muted-foreground mt-2">
59
- +{blockers.length - 3} more
60
- </p>
61
- )}
62
- </>
18
+ <div className={cn(
19
+ 'relative overflow-hidden rounded-xl border bg-card p-4',
20
+ className
21
+ )}>
22
+ <div className="flex items-center justify-between mb-3">
23
+ <div className="flex items-center gap-2">
24
+ <AlertTriangle className="h-4 w-4 text-muted-foreground" />
25
+ <span className="text-xs font-bold uppercase tracking-[0.15em] text-muted-foreground">
26
+ Blockers
27
+ </span>
28
+ </div>
29
+ {hasBlockers && (
30
+ <span className="text-xs font-bold text-muted-foreground tabular-nums">
31
+ {blockers.length}
32
+ </span>
63
33
  )}
64
34
  </div>
65
- </BentoCard>
35
+
36
+ {!hasBlockers ? (
37
+ <div className="flex items-center gap-3 py-2">
38
+ <CheckCircle2 className="h-5 w-5 text-muted-foreground" />
39
+ <div>
40
+ <p className="text-sm font-medium">No blockers</p>
41
+ <p className="text-xs text-muted-foreground">Keep the momentum</p>
42
+ </div>
43
+ </div>
44
+ ) : (
45
+ <div className="space-y-2">
46
+ {displayBlockers.map((blocker, i) => (
47
+ <div
48
+ key={i}
49
+ className="p-2.5 rounded-lg bg-muted/50 border"
50
+ >
51
+ <p className="text-sm font-medium">{blocker.task}</p>
52
+ <p className="text-xs text-muted-foreground mt-0.5">
53
+ {blocker.reason}
54
+ </p>
55
+ <div className="flex items-center gap-1 mt-1.5">
56
+ <Clock className="h-3 w-3 text-muted-foreground" />
57
+ <span className="text-xs font-medium text-muted-foreground">
58
+ {blocker.daysBlocked} day{blocker.daysBlocked !== 1 ? 's' : ''} blocked
59
+ </span>
60
+ </div>
61
+ </div>
62
+ ))}
63
+ {hasMore && (
64
+ <ExpandButton
65
+ expanded={expanded}
66
+ totalCount={blockers.length}
67
+ collapsedLimit={COLLAPSED_LIMIT}
68
+ onToggle={() => setExpanded(!expanded)}
69
+ />
70
+ )}
71
+ </div>
72
+ )}
73
+ </div>
66
74
  )
67
75
  }
@@ -7,5 +7,6 @@ export interface Blocker {
7
7
 
8
8
  export interface BlockersCardProps {
9
9
  blockers: Blocker[]
10
+ codeHref?: string
10
11
  className?: string
11
12
  }