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
@@ -0,0 +1,95 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+ import { MasonryGrid } from '@/components/MasonryGrid'
5
+ import { NowCard } from '@/components/NowCard'
6
+ import { VelocityCard } from '@/components/VelocityCard'
7
+ import { StreakCard } from '@/components/StreakCard'
8
+ import { QueueCard } from '@/components/QueueCard'
9
+ import { ShipsCard } from '@/components/ShipsCard'
10
+ import { IdeasCard } from '@/components/IdeasCard'
11
+ import { AgentsCard } from '@/components/AgentsCard'
12
+ import { RoadmapCard } from '@/components/RoadmapCard'
13
+ import { BlockersCard } from '@/components/BlockersCard'
14
+ import { RecoverCard, type AbandonedSession } from '@/components/RecoverCard'
15
+ import { ActivityTimeline } from '@/components/ActivityTimeline'
16
+ import type { TimelineEvent } from '@/lib/parse-prjct-files'
17
+
18
+ interface StatsMasonryProps {
19
+ projectId: string
20
+ currentTask: any
21
+ velocity: number
22
+ weeklyVelocityData: number[]
23
+ velocityChange: number
24
+ estimateAccuracy?: number
25
+ roadmap: any
26
+ queue: any[]
27
+ shipped: any[]
28
+ totalShips: number
29
+ streak: number
30
+ blockers: any[]
31
+ ideas: any[]
32
+ agents: any[]
33
+ timeline: TimelineEvent[]
34
+ }
35
+
36
+ export function StatsMasonry({
37
+ projectId,
38
+ currentTask,
39
+ velocity,
40
+ weeklyVelocityData,
41
+ velocityChange,
42
+ estimateAccuracy,
43
+ roadmap,
44
+ queue,
45
+ shipped,
46
+ totalShips,
47
+ streak,
48
+ blockers,
49
+ ideas,
50
+ agents,
51
+ timeline,
52
+ }: StatsMasonryProps) {
53
+ const codeHref = `/project/${projectId}/code`
54
+ const [abandonedSessions, setAbandonedSessions] = useState<AbandonedSession[]>([])
55
+
56
+ // Fetch abandoned sessions from API
57
+ useEffect(() => {
58
+ async function fetchAbandonedSessions() {
59
+ try {
60
+ const res = await fetch(`/api/sessions/current?projectId=${projectId}`)
61
+ const data = await res.json()
62
+ if (data.success && data.data.abandonedSessions) {
63
+ setAbandonedSessions(data.data.abandonedSessions)
64
+ }
65
+ } catch {
66
+ // Silently fail - abandoned sessions are not critical
67
+ }
68
+ }
69
+ fetchAbandonedSessions()
70
+ }, [projectId])
71
+
72
+ return (
73
+ <MasonryGrid>
74
+ {/* Show RecoverCard first if there are abandoned sessions */}
75
+ {abandonedSessions.length > 0 && (
76
+ <RecoverCard abandonedSessions={abandonedSessions} codeHref={codeHref} />
77
+ )}
78
+ <NowCard currentTask={currentTask} codeHref={codeHref} />
79
+ <VelocityCard
80
+ tasksPerDay={velocity}
81
+ weeklyData={weeklyVelocityData}
82
+ change={velocityChange}
83
+ estimateAccuracy={estimateAccuracy}
84
+ />
85
+ <RoadmapCard roadmap={roadmap} codeHref={codeHref} />
86
+ <QueueCard queue={queue} codeHref={codeHref} />
87
+ <ShipsCard ships={shipped} totalShips={totalShips} codeHref={codeHref} />
88
+ <StreakCard streak={streak} />
89
+ <BlockersCard blockers={blockers} codeHref={codeHref} />
90
+ <IdeasCard ideas={ideas} codeHref={codeHref} />
91
+ <AgentsCard agents={agents} codeHref={codeHref} />
92
+ <ActivityTimeline timeline={timeline} />
93
+ </MasonryGrid>
94
+ )
95
+ }
@@ -0,0 +1 @@
1
+ export { StatsMasonry } from './StatsMasonry'
@@ -1,53 +1,55 @@
1
- import { BentoCard } from '@/components/BentoCard'
2
- import { Flame } from 'lucide-react'
1
+ 'use client'
2
+
3
+ import { Flame, Calendar } from 'lucide-react'
3
4
  import { cn } from '@/lib/utils'
4
- import { STREAK_HOT_THRESHOLD, STREAK_ON_FIRE_THRESHOLD } from './StreakCard.constants'
5
5
  import type { StreakCardProps } from './StreakCard.types'
6
6
 
7
- export function StreakCard({ streak, className }: StreakCardProps) {
8
- const isHot = streak >= STREAK_HOT_THRESHOLD
9
- const isOnFire = streak >= STREAK_ON_FIRE_THRESHOLD
7
+ function getStreakColor(streak: number): string {
8
+ if (streak >= 7) return 'text-emerald-600 dark:text-emerald-400'
9
+ if (streak >= 3) return 'text-amber-600 dark:text-amber-400'
10
+ return 'text-red-600 dark:text-red-400'
11
+ }
10
12
 
13
+ export function StreakCard({ streak, className }: StreakCardProps) {
11
14
  return (
12
- <BentoCard
13
- size="1x1"
14
- title="Streak"
15
- icon={Flame}
16
- accentColor={isOnFire ? 'warning' : 'default'}
17
- className={className}
18
- >
19
- <div className="flex flex-col h-full justify-between">
20
- <div className="flex items-center gap-3">
21
- <Flame
22
- className={cn(
23
- 'h-8 w-8 transition-colors',
24
- isOnFire ? 'text-orange-500' : isHot ? 'text-amber-500' : 'text-muted-foreground'
25
- )}
26
- />
27
- <div>
28
- <p className="text-3xl font-bold tabular-nums">{streak}</p>
29
- <p className="text-xs text-muted-foreground">day{streak !== 1 ? 's' : ''}</p>
30
- </div>
15
+ <div className={cn(
16
+ 'relative overflow-hidden rounded-xl border bg-card p-4',
17
+ className
18
+ )}>
19
+ <div className="flex items-center justify-between mb-3">
20
+ <div className="flex items-center gap-2">
21
+ <Flame className="h-4 w-4 text-muted-foreground" />
22
+ <span className="text-xs font-bold uppercase tracking-[0.15em] text-muted-foreground">
23
+ Streak
24
+ </span>
31
25
  </div>
26
+ </div>
32
27
 
33
- <div className="flex gap-1 mt-3">
28
+ <div className="flex items-center gap-4">
29
+ <Flame className={cn("h-10 w-10", getStreakColor(streak))} />
30
+ <div>
31
+ <p className="text-3xl font-bold tabular-nums">{streak}</p>
32
+ <p className="text-xs text-muted-foreground">consecutive day{streak !== 1 ? 's' : ''}</p>
33
+ </div>
34
+ </div>
35
+
36
+ <div className="mt-4">
37
+ <div className="flex items-center gap-1.5 mb-2">
38
+ <Calendar className="h-3 w-3 text-muted-foreground" />
39
+ <span className="text-xs text-muted-foreground">Last 7 days</span>
40
+ </div>
41
+ <div className="flex gap-1">
34
42
  {Array.from({ length: 7 }).map((_, i) => (
35
43
  <div
36
44
  key={i}
37
45
  className={cn(
38
- 'h-1.5 flex-1 rounded-full transition-colors',
39
- i < streak
40
- ? isOnFire
41
- ? 'bg-orange-500'
42
- : isHot
43
- ? 'bg-amber-500'
44
- : 'bg-foreground'
45
- : 'bg-muted'
46
+ 'h-2 flex-1 rounded-full transition-colors',
47
+ i < streak ? 'bg-foreground' : 'bg-muted'
46
48
  )}
47
49
  />
48
50
  ))}
49
51
  </div>
50
52
  </div>
51
- </BentoCard>
53
+ </div>
52
54
  )
53
55
  }
@@ -7,7 +7,7 @@ export function TasksCounter({ count }: TasksCounterProps) {
7
7
  {count}
8
8
  </span>
9
9
  <span className="text-sm sm:text-base md:text-lg text-muted-foreground">
10
- tasks completed
10
+ shipped
11
11
  </span>
12
12
  </div>
13
13
  )
@@ -1,17 +1,25 @@
1
1
  import { Badge } from '@/components/ui/badge'
2
2
 
3
3
  interface TechStackBadgesProps {
4
- techStack: string[]
4
+ techStack: string[] | Record<string, string[]> | undefined
5
5
  max?: number
6
6
  }
7
7
 
8
+ function normalizeTechStack(techStack: string[] | Record<string, string[]> | undefined): string[] {
9
+ if (!techStack) return []
10
+ if (Array.isArray(techStack)) return techStack
11
+ // Handle object format: { languages: [...], frameworks: [...] }
12
+ return Object.values(techStack).flat()
13
+ }
14
+
8
15
  export function TechStackBadges({ techStack, max = 4 }: TechStackBadgesProps) {
9
- if (!techStack || techStack.length === 0) return null
16
+ const normalized = normalizeTechStack(techStack)
17
+ if (normalized.length === 0) return null
10
18
 
11
19
  return (
12
20
  <div className="flex gap-1">
13
- {techStack.slice(0, max).map((tech) => (
14
- <Badge key={tech} variant="secondary" className="text-[10px] px-1.5 py-0">
21
+ {normalized.slice(0, max).map((tech) => (
22
+ <Badge key={tech} variant="secondary" className="text-xs px-1.5 py-0">
15
23
  {tech}
16
24
  </Badge>
17
25
  ))}
@@ -0,0 +1,29 @@
1
+ 'use client'
2
+
3
+ import { ChevronUp } from 'lucide-react'
4
+ import { cn } from '@/lib/utils'
5
+
6
+ interface DockToggleTabProps {
7
+ sessionCount: number
8
+ connectedCount: number
9
+ onClick: () => void
10
+ }
11
+
12
+ export function DockToggleTab({ sessionCount, connectedCount, onClick }: DockToggleTabProps) {
13
+ const hasActiveSessions = sessionCount > 0
14
+
15
+ return (
16
+ <button
17
+ onClick={onClick}
18
+ className={cn(
19
+ 'fixed bottom-0 left-1/2 -translate-x-1/2 z-40',
20
+ 'flex items-center gap-2 px-4 py-2 rounded-t-lg',
21
+ 'bg-orange-500 hover:bg-orange-600 text-white',
22
+ 'shadow-lg transition-all duration-200',
23
+ 'hover:-translate-y-0.5'
24
+ )}
25
+ >
26
+ <ChevronUp className="w-4 h-4" />
27
+ </button>
28
+ )
29
+ }
@@ -0,0 +1,386 @@
1
+ 'use client'
2
+
3
+ /**
4
+ * TerminalDock v3.1
5
+ * - Drawer with native overlay when >50% (respects sidebar)
6
+ * - Commands on left side inside drawer
7
+ * - Chrome-style tabs
8
+ * - Rename tabs via double-click
9
+ */
10
+
11
+ import { useCallback, useEffect, useRef, useState } from 'react'
12
+ import { usePathname, useParams } from 'next/navigation'
13
+ import { useGlobalTerminal } from '@/context/GlobalTerminalContext'
14
+ import { TerminalDockTab } from './TerminalDockTab'
15
+ import { TerminalTabBar } from './TerminalTabBar'
16
+ import { DockToggleTab } from './DockToggleTab'
17
+ import { ProjectSelectorModal } from '@/components/ProjectSelectorModal'
18
+ import { CommandBar } from '@/components/CommandBar'
19
+ import { cn } from '@/lib/utils'
20
+ import { Minus, SplitSquareHorizontal } from 'lucide-react'
21
+ import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/components/ui/tooltip'
22
+ import {
23
+ Drawer,
24
+ DrawerContent,
25
+ DrawerTitle,
26
+ } from '@/components/ui/drawer'
27
+ import { VisuallyHidden } from '@radix-ui/react-visually-hidden'
28
+ import {
29
+ Panel,
30
+ PanelGroup,
31
+ PanelResizeHandle,
32
+ } from 'react-resizable-panels'
33
+
34
+ // Threshold for showing drawer with overlay (50% of viewport)
35
+ const DRAWER_THRESHOLD = 0.5
36
+
37
+ export function TerminalDock() {
38
+ const pathname = usePathname()
39
+ const params = useParams()
40
+ const {
41
+ isDockOpen,
42
+ dockHeight,
43
+ isFullScreen,
44
+ isSplitEnabled,
45
+ setDockHeight,
46
+ openDock,
47
+ closeDock,
48
+ setSplitEnabled,
49
+ projectSessions,
50
+ activeSessionId,
51
+ secondActiveSessionId,
52
+ switchSession,
53
+ closeSession,
54
+ createSessionForProject,
55
+ sendCommandToActive,
56
+ getActiveSession,
57
+ getAllSessions,
58
+ getLeftPanelSessions,
59
+ getRightPanelSessions,
60
+ getTotalSessionCount,
61
+ getConnectedSessionCount,
62
+ updateSession,
63
+ } = useGlobalTerminal()
64
+
65
+ const dockRef = useRef<HTMLDivElement>(null)
66
+ const [isResizing, setIsResizing] = useState(false)
67
+ const [showProjectSelector, setShowProjectSelector] = useState(false)
68
+ const [projectSelectorPanel, setProjectSelectorPanel] = useState<'left' | 'right'>('left')
69
+ const [viewportHeight, setViewportHeight] = useState(800)
70
+
71
+
72
+ // Update viewport height on mount and resize
73
+ useEffect(() => {
74
+ const updateViewport = () => setViewportHeight(window.innerHeight)
75
+ updateViewport()
76
+ window.addEventListener('resize', updateViewport)
77
+ return () => window.removeEventListener('resize', updateViewport)
78
+ }, [])
79
+
80
+ // Get all sessions
81
+ const allSessions = getAllSessions()
82
+ const activeSession = getActiveSession()
83
+ const isConnected = activeSession?.isConnected ?? false
84
+ const totalSessions = getTotalSessionCount()
85
+ const connectedSessions = getConnectedSessionCount()
86
+
87
+ // Always use drawer mode
88
+ const useDrawerMode = true
89
+
90
+ // Hide dock on code page (full-screen terminal)
91
+ const isCodePage = pathname?.includes('/code')
92
+
93
+ // Get current project context from URL
94
+ const currentProjectId = params?.id as string | undefined
95
+
96
+ // Resize handlers (only for non-drawer mode)
97
+ const startResize = useCallback((e: React.MouseEvent) => {
98
+ e.preventDefault()
99
+ setIsResizing(true)
100
+ }, [])
101
+
102
+ useEffect(() => {
103
+ if (!isResizing) return
104
+
105
+ const handleMouseMove = (e: MouseEvent) => {
106
+ const newHeight = window.innerHeight - e.clientY
107
+ setDockHeight(newHeight)
108
+ }
109
+
110
+ const handleMouseUp = () => {
111
+ setIsResizing(false)
112
+ }
113
+
114
+ document.addEventListener('mousemove', handleMouseMove)
115
+ document.addEventListener('mouseup', handleMouseUp)
116
+
117
+ return () => {
118
+ document.removeEventListener('mousemove', handleMouseMove)
119
+ document.removeEventListener('mouseup', handleMouseUp)
120
+ }
121
+ }, [isResizing, setDockHeight])
122
+
123
+ // Handle new terminal - creates session directly in the specified panel
124
+ const handleNewTerminal = useCallback((panel: 'left' | 'right' = 'left') => {
125
+ if (currentProjectId) {
126
+ const project = projectSessions.get(currentProjectId)
127
+ if (project) {
128
+ createSessionForProject(project.projectId, project.projectName, project.projectPath, panel)
129
+ return
130
+ }
131
+ }
132
+ setProjectSelectorPanel(panel)
133
+ setShowProjectSelector(true)
134
+ }, [currentProjectId, projectSessions, createSessionForProject])
135
+
136
+ // Handle project selection from modal
137
+ const handleSelectProject = useCallback((projectId: string, projectName: string, projectPath: string) => {
138
+ createSessionForProject(projectId, projectName, projectPath, projectSelectorPanel)
139
+ setShowProjectSelector(false)
140
+ }, [createSessionForProject, projectSelectorPanel])
141
+
142
+ // Handle command click
143
+ const handleCommand = useCallback((cmd: string) => {
144
+ sendCommandToActive(cmd)
145
+ }, [sendCommandToActive])
146
+
147
+ // Handle close session
148
+ const handleCloseSession = useCallback((sessionId: string) => {
149
+ const disconnectKey = `terminal_disconnect_${sessionId}`
150
+ const disconnectFn = (window as unknown as Record<string, () => void>)[disconnectKey]
151
+ if (disconnectFn) {
152
+ disconnectFn()
153
+ }
154
+ closeSession(sessionId)
155
+ }, [closeSession])
156
+
157
+ // Handle rename session
158
+ const handleRenameSession = useCallback((sessionId: string, newLabel: string) => {
159
+ updateSession(sessionId, { label: newLabel })
160
+ }, [updateSession])
161
+
162
+ // Don't render if on code page (full-screen mode)
163
+ if (isCodePage || isFullScreen) {
164
+ return null
165
+ }
166
+
167
+ // Show toggle tab when dock is closed
168
+ if (!isDockOpen) {
169
+ return (
170
+ <DockToggleTab
171
+ sessionCount={totalSessions}
172
+ connectedCount={connectedSessions}
173
+ onClick={openDock}
174
+ />
175
+ )
176
+ }
177
+
178
+
179
+ // Terminal content (sessions)
180
+ const TerminalContent = (
181
+ <div className="flex-1 relative overflow-hidden">
182
+ {allSessions.length === 0 ? (
183
+ <div className="flex flex-col items-center justify-center h-full text-muted-foreground text-sm gap-2">
184
+ <span>No terminal sessions</span>
185
+ <button
186
+ onClick={() => handleNewTerminal('left')}
187
+ className="text-muted-foreground hover:text-foreground underline"
188
+ >
189
+ Open a terminal
190
+ </button>
191
+ </div>
192
+ ) : isSplitEnabled ? (
193
+ <PanelGroup direction="horizontal">
194
+ <Panel defaultSize={50} minSize={20}>
195
+ <div className="h-full flex flex-col">
196
+ <div className="border-b border-border bg-muted/30">
197
+ <TerminalTabBar
198
+ sessions={getLeftPanelSessions()}
199
+ activeSessionId={activeSessionId}
200
+ onSwitchSession={(id) => switchSession(id, 'left')}
201
+ onCloseSession={handleCloseSession}
202
+ onNewTerminal={() => handleNewTerminal('left')}
203
+ onRenameSession={handleRenameSession}
204
+ />
205
+ </div>
206
+ <div className="flex-1 relative">
207
+ {getLeftPanelSessions().length > 0 ? (
208
+ getLeftPanelSessions().map((session) => (
209
+ <TerminalDockTab
210
+ key={session.id}
211
+ session={session}
212
+ isActive={session.id === activeSessionId}
213
+ />
214
+ ))
215
+ ) : (
216
+ <div className="absolute inset-0 flex items-center justify-center bg-card/95 text-muted-foreground text-sm">
217
+ <button
218
+ onClick={() => handleNewTerminal('left')}
219
+ className="text-muted-foreground hover:text-foreground underline"
220
+ >
221
+ Open terminal in left panel
222
+ </button>
223
+ </div>
224
+ )}
225
+ </div>
226
+ </div>
227
+ </Panel>
228
+
229
+ <PanelResizeHandle className="w-1 bg-border hover:bg-muted transition-colors" />
230
+
231
+ <Panel defaultSize={50} minSize={20}>
232
+ <div className="h-full flex flex-col">
233
+ <div className="border-b border-border bg-muted/30">
234
+ <TerminalTabBar
235
+ sessions={getRightPanelSessions()}
236
+ activeSessionId={secondActiveSessionId}
237
+ onSwitchSession={(id) => switchSession(id, 'right')}
238
+ onCloseSession={handleCloseSession}
239
+ onNewTerminal={() => handleNewTerminal('right')}
240
+ onRenameSession={handleRenameSession}
241
+ />
242
+ </div>
243
+ <div className="flex-1 relative">
244
+ {getRightPanelSessions().length > 0 ? (
245
+ getRightPanelSessions().map((session) => (
246
+ <TerminalDockTab
247
+ key={`right-${session.id}`}
248
+ session={session}
249
+ isActive={session.id === secondActiveSessionId}
250
+ />
251
+ ))
252
+ ) : (
253
+ <div className="absolute inset-0 flex items-center justify-center bg-card/95 text-muted-foreground text-sm">
254
+ <button
255
+ onClick={() => handleNewTerminal('right')}
256
+ className="text-muted-foreground hover:text-foreground underline"
257
+ >
258
+ Open terminal in right panel
259
+ </button>
260
+ </div>
261
+ )}
262
+ </div>
263
+ </div>
264
+ </Panel>
265
+ </PanelGroup>
266
+ ) : (
267
+ <div className="h-full flex flex-col">
268
+ <div className="border-b border-border bg-muted/30">
269
+ <TerminalTabBar
270
+ sessions={allSessions}
271
+ activeSessionId={activeSessionId}
272
+ onSwitchSession={(id) => switchSession(id)}
273
+ onCloseSession={handleCloseSession}
274
+ onNewTerminal={() => handleNewTerminal('left')}
275
+ onRenameSession={handleRenameSession}
276
+ />
277
+ </div>
278
+ <div className="flex-1 relative">
279
+ {allSessions.map((session) => (
280
+ <TerminalDockTab
281
+ key={session.id}
282
+ session={session}
283
+ isActive={session.id === activeSessionId}
284
+ />
285
+ ))}
286
+ </div>
287
+ </div>
288
+ )}
289
+ </div>
290
+ )
291
+
292
+ // Header with split/minimize (right side)
293
+ const HeaderActions = (
294
+ <div className="flex items-center gap-1 px-2">
295
+ <Tooltip>
296
+ <TooltipTrigger asChild>
297
+ <button
298
+ onClick={() => setSplitEnabled(!isSplitEnabled)}
299
+ className={cn(
300
+ 'p-1.5 rounded transition-colors',
301
+ isSplitEnabled
302
+ ? 'bg-accent text-accent-foreground'
303
+ : 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
304
+ )}
305
+ >
306
+ <SplitSquareHorizontal className="w-4 h-4" />
307
+ </button>
308
+ </TooltipTrigger>
309
+ <TooltipContent>{isSplitEnabled ? 'Single view' : 'Split view'}</TooltipContent>
310
+ </Tooltip>
311
+
312
+ <Tooltip>
313
+ <TooltipTrigger asChild>
314
+ <button
315
+ onClick={closeDock}
316
+ className="p-1.5 rounded text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
317
+ >
318
+ <Minus className="w-4 h-4" />
319
+ </button>
320
+ </TooltipTrigger>
321
+ <TooltipContent>Minimize</TooltipContent>
322
+ </Tooltip>
323
+ </div>
324
+ )
325
+
326
+ return (
327
+ <TooltipProvider>
328
+ {useDrawerMode ? (
329
+ // Simple drawer
330
+ <Drawer open={isDockOpen} onOpenChange={(open) => !open && closeDock()}>
331
+ <DrawerContent className="h-[60vh] flex flex-col">
332
+ <VisuallyHidden>
333
+ <DrawerTitle>Terminal</DrawerTitle>
334
+ </VisuallyHidden>
335
+
336
+ {/* Header: commands left, actions right */}
337
+ <div className="flex items-center justify-between border-b border-border bg-card/95 shrink-0">
338
+ <CommandBar isConnected={isConnected} onCommand={handleCommand} />
339
+ {HeaderActions}
340
+ </div>
341
+
342
+ {TerminalContent}
343
+ </DrawerContent>
344
+ </Drawer>
345
+ ) : (
346
+ // Regular dock when <=50%
347
+ <div
348
+ ref={dockRef}
349
+ className={cn(
350
+ 'relative flex flex-col shrink-0',
351
+ 'bg-card border-t border-border',
352
+ isResizing && 'select-none'
353
+ )}
354
+ style={{ height: dockHeight }}
355
+ >
356
+ {/* Resize Handle */}
357
+ <div
358
+ className={cn(
359
+ 'absolute top-0 left-0 right-0 h-2 cursor-ns-resize z-10',
360
+ 'hover:bg-muted transition-colors',
361
+ 'before:absolute before:inset-x-0 before:top-0 before:h-px before:bg-border',
362
+ isResizing && 'bg-muted'
363
+ )}
364
+ onMouseDown={startResize}
365
+ >
366
+ <div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 w-12 h-1 rounded-full bg-muted-foreground/30" />
367
+ </div>
368
+
369
+ {/* Header: commands left, actions right */}
370
+ <div className="flex items-center justify-between border-b border-border bg-card/95 shrink-0 mt-2">
371
+ <CommandBar isConnected={isConnected} onCommand={handleCommand} />
372
+ {HeaderActions}
373
+ </div>
374
+
375
+ {TerminalContent}
376
+ </div>
377
+ )}
378
+
379
+ <ProjectSelectorModal
380
+ isOpen={showProjectSelector}
381
+ onClose={() => setShowProjectSelector(false)}
382
+ onSelectProject={handleSelectProject}
383
+ />
384
+ </TooltipProvider>
385
+ )
386
+ }