prjct-cli 0.11.0 → 0.11.2

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 (81) hide show
  1. package/bin/serve.js +90 -26
  2. package/package.json +11 -1
  3. package/packages/shared/dist/index.d.ts +615 -0
  4. package/packages/shared/dist/index.js +204 -0
  5. package/packages/shared/package.json +29 -0
  6. package/packages/shared/src/index.ts +9 -0
  7. package/packages/shared/src/schemas.ts +124 -0
  8. package/packages/shared/src/types.ts +187 -0
  9. package/packages/shared/src/utils.ts +148 -0
  10. package/packages/shared/tsconfig.json +18 -0
  11. package/packages/web/README.md +36 -0
  12. package/packages/web/app/api/claude/sessions/route.ts +44 -0
  13. package/packages/web/app/api/claude/status/route.ts +34 -0
  14. package/packages/web/app/api/projects/[id]/delete/route.ts +21 -0
  15. package/packages/web/app/api/projects/[id]/icon/route.ts +33 -0
  16. package/packages/web/app/api/projects/[id]/route.ts +29 -0
  17. package/packages/web/app/api/projects/[id]/stats/route.ts +36 -0
  18. package/packages/web/app/api/projects/[id]/status/route.ts +21 -0
  19. package/packages/web/app/api/projects/route.ts +16 -0
  20. package/packages/web/app/api/sessions/history/route.ts +122 -0
  21. package/packages/web/app/api/stats/route.ts +38 -0
  22. package/packages/web/app/error.tsx +34 -0
  23. package/packages/web/app/favicon.ico +0 -0
  24. package/packages/web/app/globals.css +155 -0
  25. package/packages/web/app/layout.tsx +43 -0
  26. package/packages/web/app/loading.tsx +7 -0
  27. package/packages/web/app/not-found.tsx +25 -0
  28. package/packages/web/app/page.tsx +227 -0
  29. package/packages/web/app/project/[id]/error.tsx +41 -0
  30. package/packages/web/app/project/[id]/loading.tsx +9 -0
  31. package/packages/web/app/project/[id]/not-found.tsx +27 -0
  32. package/packages/web/app/project/[id]/page.tsx +253 -0
  33. package/packages/web/app/project/[id]/stats/page.tsx +447 -0
  34. package/packages/web/app/sessions/page.tsx +165 -0
  35. package/packages/web/app/settings/page.tsx +150 -0
  36. package/packages/web/components/AppSidebar.tsx +113 -0
  37. package/packages/web/components/CommandButton.tsx +39 -0
  38. package/packages/web/components/ConnectionStatus.tsx +29 -0
  39. package/packages/web/components/Logo.tsx +65 -0
  40. package/packages/web/components/MarkdownContent.tsx +123 -0
  41. package/packages/web/components/ProjectAvatar.tsx +54 -0
  42. package/packages/web/components/TechStackBadges.tsx +20 -0
  43. package/packages/web/components/TerminalTab.tsx +84 -0
  44. package/packages/web/components/TerminalTabs.tsx +210 -0
  45. package/packages/web/components/charts/SessionsChart.tsx +172 -0
  46. package/packages/web/components/providers.tsx +45 -0
  47. package/packages/web/components/ui/alert-dialog.tsx +157 -0
  48. package/packages/web/components/ui/badge.tsx +46 -0
  49. package/packages/web/components/ui/button.tsx +60 -0
  50. package/packages/web/components/ui/card.tsx +92 -0
  51. package/packages/web/components/ui/chart.tsx +385 -0
  52. package/packages/web/components/ui/dropdown-menu.tsx +257 -0
  53. package/packages/web/components/ui/scroll-area.tsx +58 -0
  54. package/packages/web/components/ui/sheet.tsx +139 -0
  55. package/packages/web/components/ui/tabs.tsx +66 -0
  56. package/packages/web/components/ui/tooltip.tsx +61 -0
  57. package/packages/web/components.json +22 -0
  58. package/packages/web/context/TerminalContext.tsx +45 -0
  59. package/packages/web/context/TerminalTabsContext.tsx +136 -0
  60. package/packages/web/eslint.config.mjs +18 -0
  61. package/packages/web/hooks/useClaudeTerminal.ts +375 -0
  62. package/packages/web/hooks/useProjectStats.ts +38 -0
  63. package/packages/web/hooks/useProjects.ts +73 -0
  64. package/packages/web/hooks/useStats.ts +28 -0
  65. package/packages/web/lib/format.ts +23 -0
  66. package/packages/web/lib/parse-prjct-files.ts +1122 -0
  67. package/packages/web/lib/projects.ts +452 -0
  68. package/packages/web/lib/pty.ts +101 -0
  69. package/packages/web/lib/query-config.ts +44 -0
  70. package/packages/web/lib/utils.ts +6 -0
  71. package/packages/web/next-env.d.ts +6 -0
  72. package/packages/web/next.config.ts +7 -0
  73. package/packages/web/package.json +53 -0
  74. package/packages/web/postcss.config.mjs +7 -0
  75. package/packages/web/public/file.svg +1 -0
  76. package/packages/web/public/globe.svg +1 -0
  77. package/packages/web/public/next.svg +1 -0
  78. package/packages/web/public/vercel.svg +1 -0
  79. package/packages/web/public/window.svg +1 -0
  80. package/packages/web/server.ts +262 -0
  81. package/packages/web/tsconfig.json +34 -0
@@ -0,0 +1,7 @@
1
+ export default function Loading() {
2
+ return (
3
+ <div className="flex items-center justify-center h-full">
4
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
5
+ </div>
6
+ )
7
+ }
@@ -0,0 +1,25 @@
1
+ import Link from 'next/link'
2
+ import { Button } from '@/components/ui/button'
3
+ import { Home } from 'lucide-react'
4
+
5
+ export default function NotFound() {
6
+ return (
7
+ <div className="flex items-center justify-center h-full">
8
+ <div className="text-center space-y-4">
9
+ <h1 className="text-6xl font-bold text-muted-foreground">404</h1>
10
+ <div>
11
+ <h2 className="text-lg font-medium">Page not found</h2>
12
+ <p className="text-sm text-muted-foreground mt-1">
13
+ The page you're looking for doesn't exist.
14
+ </p>
15
+ </div>
16
+ <Button asChild variant="outline">
17
+ <Link href="/">
18
+ <Home className="w-4 h-4 mr-2" />
19
+ Back to Dashboard
20
+ </Link>
21
+ </Button>
22
+ </div>
23
+ </div>
24
+ )
25
+ }
@@ -0,0 +1,227 @@
1
+ 'use client'
2
+
3
+ import { useState, useCallback } from 'react'
4
+ import Link from 'next/link'
5
+ import { SessionsChart } from '@/components/charts/SessionsChart'
6
+ import { Button } from '@/components/ui/button'
7
+ import { ProjectAvatar } from '@/components/ProjectAvatar'
8
+ import { TechStackBadges } from '@/components/TechStackBadges'
9
+ import { formatRelativeTime, formatPath } from '@/lib/format'
10
+ import { useProjects, useDeleteProject, type Project } from '@/hooks/useProjects'
11
+ import { useStats } from '@/hooks/useStats'
12
+ import {
13
+ MoreHorizontal,
14
+ Trash2,
15
+ AlertTriangle,
16
+ Target,
17
+ Lightbulb,
18
+ ListTodo,
19
+ Zap,
20
+ FolderGit2
21
+ } from 'lucide-react'
22
+ import {
23
+ DropdownMenu,
24
+ DropdownMenuContent,
25
+ DropdownMenuItem,
26
+ DropdownMenuTrigger,
27
+ } from '@/components/ui/dropdown-menu'
28
+ import {
29
+ AlertDialog,
30
+ AlertDialogAction,
31
+ AlertDialogCancel,
32
+ AlertDialogContent,
33
+ AlertDialogDescription,
34
+ AlertDialogFooter,
35
+ AlertDialogHeader,
36
+ AlertDialogTitle,
37
+ } from '@/components/ui/alert-dialog'
38
+
39
+ export default function Dashboard() {
40
+ const [projectToDelete, setProjectToDelete] = useState<Project | null>(null)
41
+
42
+ const { data: projects, isLoading: projectsLoading } = useProjects()
43
+ const { data: stats } = useStats()
44
+ const deleteMutation = useDeleteProject()
45
+
46
+ const handleDeleteClick = useCallback((project: Project, e: React.MouseEvent) => {
47
+ e.preventDefault()
48
+ e.stopPropagation()
49
+ setProjectToDelete(project)
50
+ }, [])
51
+
52
+ const handleConfirmDelete = useCallback(() => {
53
+ if (projectToDelete) {
54
+ deleteMutation.mutate(projectToDelete.id, {
55
+ onSuccess: () => setProjectToDelete(null)
56
+ })
57
+ }
58
+ }, [projectToDelete, deleteMutation])
59
+
60
+ if (projectsLoading) {
61
+ return (
62
+ <div className="flex items-center justify-center h-full">
63
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
64
+ </div>
65
+ )
66
+ }
67
+
68
+ const projectCount = projects?.length || 0
69
+
70
+ return (
71
+ <div className="p-8 h-full overflow-auto">
72
+ <p className="text-muted-foreground">Hola! {stats?.userName || 'Developer'}</p>
73
+
74
+ <h1 className="text-8xl font-bold tracking-tighter tabular-nums mt-2">{projectCount}</h1>
75
+ <p className="text-muted-foreground mt-1">{projectCount === 1 ? 'project' : 'projects'}</p>
76
+
77
+ <section className="mt-8">
78
+ <SessionsChart />
79
+ </section>
80
+
81
+ <section className="mt-12">
82
+ <h2 className="font-bold uppercase tracking-wide text-muted-foreground mb-6">Active Projects</h2>
83
+
84
+ {projects?.length === 0 ? (
85
+ <div className="border-2 border-dashed border-border rounded-xl p-8 text-center">
86
+ <p className="text-muted-foreground">
87
+ No projects yet. Initialize with <code className="bg-muted px-2 py-1 rounded font-mono">/p:init</code>
88
+ </p>
89
+ </div>
90
+ ) : (
91
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
92
+ {projects?.map((project: Project) => (
93
+ <ProjectCard key={project.id} project={project} onDeleteClick={handleDeleteClick} />
94
+ ))}
95
+ </div>
96
+ )}
97
+ </section>
98
+
99
+ <AlertDialog open={!!projectToDelete} onOpenChange={(open: boolean) => !open && setProjectToDelete(null)}>
100
+ <AlertDialogContent>
101
+ <AlertDialogHeader>
102
+ <AlertDialogTitle className="flex items-center gap-2">
103
+ <AlertTriangle className="w-5 h-5 text-destructive" />
104
+ Delete Project?
105
+ </AlertDialogTitle>
106
+ <AlertDialogDescription>
107
+ <strong>&quot;{projectToDelete?.name}&quot;</strong> will be moved to trash.
108
+ <br />
109
+ <span className="text-muted-foreground text-sm">
110
+ Location: ~/.prjct-cli/.trash/{projectToDelete?.id}
111
+ </span>
112
+ </AlertDialogDescription>
113
+ </AlertDialogHeader>
114
+ <AlertDialogFooter>
115
+ <AlertDialogCancel disabled={deleteMutation.isPending}>Cancel</AlertDialogCancel>
116
+ <AlertDialogAction
117
+ onClick={handleConfirmDelete}
118
+ disabled={deleteMutation.isPending}
119
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
120
+ >
121
+ {deleteMutation.isPending ? 'Deleting...' : 'Delete'}
122
+ </AlertDialogAction>
123
+ </AlertDialogFooter>
124
+ </AlertDialogContent>
125
+ </AlertDialog>
126
+ </div>
127
+ )
128
+ }
129
+
130
+ interface ProjectCardProps {
131
+ project: Project
132
+ onDeleteClick: (project: Project, e: React.MouseEvent) => void
133
+ }
134
+
135
+ function ProjectCard({ project, onDeleteClick }: ProjectCardProps) {
136
+ const hasStats = project.currentTask || project.nextTasksCount || project.ideasCount || project.lastActivity
137
+
138
+ return (
139
+ <div className="group relative bg-card border border-border rounded-lg overflow-hidden hover:border-primary/50 transition-all">
140
+ {project.hasActiveSession && <div className="absolute top-0 left-0 right-0 h-0.5 bg-green-500" />}
141
+
142
+ <Link href={`/project/${project.id}`} className="block p-4">
143
+ <div className="flex items-start gap-3">
144
+ <ProjectAvatar projectId={project.id} name={project.name} iconPath={project.iconPath} size="lg" />
145
+
146
+ <div className="flex-1 min-w-0">
147
+ <div className="flex items-center gap-2">
148
+ <h3 className="font-bold truncate">{project.name}</h3>
149
+ {project.hasActiveSession && (
150
+ <span className="flex h-2 w-2">
151
+ <span className="animate-ping absolute inline-flex h-2 w-2 rounded-full bg-green-400 opacity-75" />
152
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-green-500" />
153
+ </span>
154
+ )}
155
+ </div>
156
+ {project.repoPath && (
157
+ <p className="text-xs text-muted-foreground truncate mt-0.5 flex items-center gap-1">
158
+ <FolderGit2 className="w-3 h-3 shrink-0" />
159
+ {formatPath(project.repoPath)}
160
+ </p>
161
+ )}
162
+ </div>
163
+ </div>
164
+
165
+ {project.currentTask && (
166
+ <div className="mt-3 p-2 bg-primary/5 rounded border border-primary/10">
167
+ <div className="flex items-start gap-2">
168
+ <Target className="w-3.5 h-3.5 text-primary shrink-0 mt-0.5" />
169
+ <span className="text-sm text-foreground line-clamp-2">{project.currentTask}</span>
170
+ </div>
171
+ </div>
172
+ )}
173
+
174
+ {hasStats && (
175
+ <div className="mt-3 flex items-center gap-3 text-xs text-muted-foreground">
176
+ {(project.nextTasksCount ?? 0) > 0 && (
177
+ <div className="flex items-center gap-1">
178
+ <ListTodo className="w-3.5 h-3.5" />
179
+ <span>{project.nextTasksCount} queued</span>
180
+ </div>
181
+ )}
182
+ {(project.ideasCount ?? 0) > 0 && (
183
+ <div className="flex items-center gap-1">
184
+ <Lightbulb className="w-3.5 h-3.5" />
185
+ <span>{project.ideasCount} ideas</span>
186
+ </div>
187
+ )}
188
+ {project.lastActivity && (
189
+ <div className="flex items-center gap-1 ml-auto">
190
+ <Zap className="w-3.5 h-3.5" />
191
+ <span>{formatRelativeTime(project.lastActivity)}</span>
192
+ </div>
193
+ )}
194
+ </div>
195
+ )}
196
+
197
+ {project.techStack && project.techStack.length > 0 && (
198
+ <div className="mt-3">
199
+ <TechStackBadges techStack={project.techStack} />
200
+ </div>
201
+ )}
202
+ </Link>
203
+
204
+ <DropdownMenu>
205
+ <DropdownMenuTrigger asChild>
206
+ <Button
207
+ variant="ghost"
208
+ size="icon-sm"
209
+ className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity"
210
+ onClick={(e) => e.preventDefault()}
211
+ >
212
+ <MoreHorizontal className="w-4 h-4" />
213
+ </Button>
214
+ </DropdownMenuTrigger>
215
+ <DropdownMenuContent align="end">
216
+ <DropdownMenuItem
217
+ className="text-destructive focus:text-destructive"
218
+ onClick={(e: React.MouseEvent) => onDeleteClick(project, e)}
219
+ >
220
+ <Trash2 className="w-4 h-4 mr-2" />
221
+ Delete
222
+ </DropdownMenuItem>
223
+ </DropdownMenuContent>
224
+ </DropdownMenu>
225
+ </div>
226
+ )
227
+ }
@@ -0,0 +1,41 @@
1
+ 'use client'
2
+
3
+ import { useEffect } from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+ import { Button } from '@/components/ui/button'
6
+ import { AlertTriangle, ArrowLeft } from 'lucide-react'
7
+
8
+ export default function ProjectError({
9
+ error,
10
+ reset,
11
+ }: {
12
+ error: Error & { digest?: string }
13
+ reset: () => void
14
+ }) {
15
+ const router = useRouter()
16
+
17
+ useEffect(() => {
18
+ console.error(error)
19
+ }, [error])
20
+
21
+ return (
22
+ <div className="flex items-center justify-center h-full">
23
+ <div className="text-center space-y-4">
24
+ <div className="w-16 h-16 rounded-full bg-destructive/10 flex items-center justify-center mx-auto">
25
+ <AlertTriangle className="w-8 h-8 text-destructive" />
26
+ </div>
27
+ <div>
28
+ <h2 className="text-lg font-medium">Failed to load project</h2>
29
+ <p className="text-sm text-muted-foreground mt-1">{error.message}</p>
30
+ </div>
31
+ <div className="flex gap-2 justify-center">
32
+ <Button onClick={() => router.push('/')} variant="outline">
33
+ <ArrowLeft className="w-4 h-4 mr-2" />
34
+ Dashboard
35
+ </Button>
36
+ <Button onClick={reset}>Try again</Button>
37
+ </div>
38
+ </div>
39
+ </div>
40
+ )
41
+ }
@@ -0,0 +1,9 @@
1
+ import { Loader2 } from 'lucide-react'
2
+
3
+ export default function ProjectLoading() {
4
+ return (
5
+ <div className="flex items-center justify-center h-full">
6
+ <Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
7
+ </div>
8
+ )
9
+ }
@@ -0,0 +1,27 @@
1
+ import Link from 'next/link'
2
+ import { Button } from '@/components/ui/button'
3
+ import { FolderX, ArrowLeft } from 'lucide-react'
4
+
5
+ export default function ProjectNotFound() {
6
+ return (
7
+ <div className="flex items-center justify-center h-full">
8
+ <div className="text-center space-y-4">
9
+ <div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center mx-auto">
10
+ <FolderX className="w-8 h-8 text-muted-foreground" />
11
+ </div>
12
+ <div>
13
+ <h2 className="text-lg font-medium">Project not found</h2>
14
+ <p className="text-sm text-muted-foreground mt-1">
15
+ This project may have been deleted or moved.
16
+ </p>
17
+ </div>
18
+ <Button asChild variant="outline">
19
+ <Link href="/">
20
+ <ArrowLeft className="w-4 h-4 mr-2" />
21
+ Back to Dashboard
22
+ </Link>
23
+ </Button>
24
+ </div>
25
+ </div>
26
+ )
27
+ }
@@ -0,0 +1,253 @@
1
+ 'use client'
2
+
3
+ import { useState, use } from 'react'
4
+ import { useRouter } from 'next/navigation'
5
+ import { useProject, useDeleteProject } from '@/hooks/useProjects'
6
+ import { TerminalTabsProvider, useTerminalTabs } from '@/context/TerminalTabsContext'
7
+ import { TerminalTabs } from '@/components/TerminalTabs'
8
+ import { Button } from '@/components/ui/button'
9
+ import { Badge } from '@/components/ui/badge'
10
+ import { TooltipProvider } from '@/components/ui/tooltip'
11
+ import {
12
+ AlertDialog,
13
+ AlertDialogAction,
14
+ AlertDialogCancel,
15
+ AlertDialogContent,
16
+ AlertDialogDescription,
17
+ AlertDialogFooter,
18
+ AlertDialogHeader,
19
+ AlertDialogTitle,
20
+ } from '@/components/ui/alert-dialog'
21
+ import { ProjectAvatar } from '@/components/ProjectAvatar'
22
+ import { TechStackBadges } from '@/components/TechStackBadges'
23
+ import { CommandButton } from '@/components/CommandButton'
24
+ import { formatPath } from '@/lib/format'
25
+ import {
26
+ Loader2,
27
+ Play,
28
+ Target,
29
+ Lightbulb,
30
+ ListTodo,
31
+ Rocket,
32
+ Sparkles,
33
+ CheckCircle2,
34
+ AlertTriangle,
35
+ Trash2,
36
+ ArrowLeft,
37
+ FolderGit2,
38
+ Pause,
39
+ BarChart3,
40
+ TrendingUp,
41
+ Activity,
42
+ History,
43
+ Undo2,
44
+ Redo2
45
+ } from 'lucide-react'
46
+
47
+ // Commands ordered by real developer workflow
48
+ const WORKFLOW_COMMANDS = [
49
+ { cmd: 'p. now', icon: Target, tip: 'Set task', group: 'work' },
50
+ { cmd: 'p. done', icon: CheckCircle2, tip: 'Complete', group: 'work' },
51
+ { cmd: 'p. pause', icon: Pause, tip: 'Pause', group: 'session' },
52
+ { cmd: 'p. resume', icon: Play, tip: 'Resume', group: 'session' },
53
+ { cmd: 'p. feature', icon: Sparkles, tip: 'Feature', group: 'plan' },
54
+ { cmd: 'p. idea', icon: Lightbulb, tip: 'Idea', group: 'plan' },
55
+ { cmd: 'p. next', icon: ListTodo, tip: 'Queue', group: 'plan' },
56
+ { cmd: 'p. ship', icon: Rocket, tip: 'Ship', group: 'ship' },
57
+ { cmd: 'p. recap', icon: BarChart3, tip: 'Recap', group: 'status' },
58
+ { cmd: 'p. progress', icon: TrendingUp, tip: 'Progress', group: 'status' },
59
+ { cmd: 'p. status', icon: Activity, tip: 'Status', group: 'status' },
60
+ { cmd: 'p. history', icon: History, tip: 'History', group: 'status' },
61
+ { cmd: 'p. undo', icon: Undo2, tip: 'Undo', group: 'recovery' },
62
+ { cmd: 'p. redo', icon: Redo2, tip: 'Redo', group: 'recovery' },
63
+ ] as const
64
+
65
+ const COMMAND_GROUPS = ['work', 'session', 'plan', 'ship', 'status', 'recovery'] as const
66
+
67
+ // Inner component that uses the terminal context
68
+ function ProjectPageContent({ projectId, project }: { projectId: string; project: NonNullable<ReturnType<typeof useProject>['data']> }) {
69
+ const router = useRouter()
70
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
71
+ const deleteMutation = useDeleteProject()
72
+
73
+ const {
74
+ sessions,
75
+ sendCommandToActive,
76
+ getActiveSession
77
+ } = useTerminalTabs()
78
+
79
+ const activeSession = getActiveSession()
80
+ const hasActiveSessions = sessions.length > 0
81
+ const isActiveConnected = activeSession?.isConnected ?? false
82
+
83
+ return (
84
+ <div className="h-full">
85
+ <TooltipProvider>
86
+ <div className="flex h-full">
87
+ {/* Sidebar */}
88
+ <aside className="w-14 border-r border-border flex flex-col bg-card/50 items-center">
89
+ <div className="h-14 flex items-center justify-center border-b border-border w-full">
90
+ <ProjectAvatar
91
+ projectId={projectId}
92
+ name={project.name || projectId}
93
+ iconPath={project.iconPath}
94
+ />
95
+ </div>
96
+
97
+ <div className="flex-1 flex flex-col gap-1 overflow-auto py-3">
98
+ {/* Stats button - navigates to stats page */}
99
+ <CommandButton
100
+ cmd="Project stats"
101
+ icon={BarChart3}
102
+ tip="Stats"
103
+ disabled={false}
104
+ onClick={() => router.push(`/project/${projectId}/stats`)}
105
+ />
106
+ <div className="border-b border-border w-8 my-2 mx-auto" />
107
+
108
+ {COMMAND_GROUPS.map((group, groupIndex) => (
109
+ <div key={group} className="flex flex-col items-center">
110
+ {WORKFLOW_COMMANDS.filter(c => c.group === group).map(({ cmd, icon, tip }) => (
111
+ <CommandButton
112
+ key={cmd}
113
+ cmd={cmd}
114
+ icon={icon}
115
+ tip={tip}
116
+ disabled={!isActiveConnected}
117
+ onClick={() => sendCommandToActive(cmd)}
118
+ />
119
+ ))}
120
+ {groupIndex < COMMAND_GROUPS.length - 1 && (
121
+ <div className="border-b border-border w-8 my-2" />
122
+ )}
123
+ </div>
124
+ ))}
125
+ </div>
126
+ </aside>
127
+
128
+ {/* Main */}
129
+ <main className="flex-1 flex flex-col">
130
+ <header className="h-14 flex items-center justify-between px-4 border-b border-border bg-card">
131
+ <div className="flex items-center gap-4">
132
+ <div className="flex flex-col">
133
+ <div className="flex items-center gap-2">
134
+ <span className="font-bold leading-tight">{project.name || projectId}</span>
135
+ {project.version && (
136
+ <Badge variant="outline" className="text-[10px] px-1.5 py-0 font-mono">
137
+ v{project.version}
138
+ </Badge>
139
+ )}
140
+ </div>
141
+ {project.repoPath && (
142
+ <span className="text-xs text-muted-foreground leading-tight flex items-center gap-1">
143
+ <FolderGit2 className="w-3 h-3" />
144
+ {formatPath(project.repoPath)}
145
+ </span>
146
+ )}
147
+ </div>
148
+ {hasActiveSessions && (
149
+ <Badge variant="outline" className="text-green-500 border-green-500/50">
150
+ {sessions.filter(s => s.isConnected).length} active
151
+ </Badge>
152
+ )}
153
+ </div>
154
+ <div className="flex items-center gap-4">
155
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
156
+ {project.stack && <span>{project.stack}</span>}
157
+ {project.filesCount && (
158
+ <span><span className="font-medium text-foreground">{project.filesCount}</span> files</span>
159
+ )}
160
+ {project.commitsCount && (
161
+ <span><span className="font-medium text-foreground">{project.commitsCount}</span> commits</span>
162
+ )}
163
+ </div>
164
+ <TechStackBadges techStack={project.techStack || []} />
165
+ </div>
166
+ </header>
167
+
168
+ {/* Terminal tabs area */}
169
+ <div className="flex-1">
170
+ <TerminalTabs projectDir={project.repoPath || project.path || '/tmp'} />
171
+ </div>
172
+ </main>
173
+ </div>
174
+
175
+ </TooltipProvider>
176
+ </div>
177
+ )
178
+ }
179
+
180
+ export default function ProjectPage({ params }: { params: Promise<{ id: string }> }) {
181
+ const { id: projectId } = use(params)
182
+ const router = useRouter()
183
+ const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
184
+
185
+ const { data: project, isLoading: projectLoading } = useProject(projectId)
186
+ const deleteMutation = useDeleteProject()
187
+
188
+ if (projectLoading) {
189
+ return (
190
+ <div className="flex items-center justify-center h-full">
191
+ <Loader2 className="w-8 h-8 animate-spin text-muted-foreground" />
192
+ </div>
193
+ )
194
+ }
195
+
196
+ if (!project) {
197
+ return (
198
+ <div className="flex items-center justify-center h-full">
199
+ <div className="text-center space-y-4">
200
+ <div className="w-16 h-16 rounded-full bg-muted flex items-center justify-center mx-auto">
201
+ <AlertTriangle className="w-8 h-8 text-muted-foreground" />
202
+ </div>
203
+ <div>
204
+ <h2 className="text-lg font-medium">Project not found</h2>
205
+ <p className="text-sm text-muted-foreground mt-1">ID: {projectId}</p>
206
+ </div>
207
+ <div className="flex gap-2 justify-center">
208
+ <Button variant="outline" onClick={() => router.push('/')}>
209
+ <ArrowLeft className="w-4 h-4 mr-2" />
210
+ Back to Dashboard
211
+ </Button>
212
+ <Button variant="destructive" onClick={() => setShowDeleteConfirm(true)}>
213
+ <Trash2 className="w-4 h-4 mr-2" />
214
+ Delete from Storage
215
+ </Button>
216
+ </div>
217
+
218
+ <AlertDialog open={showDeleteConfirm} onOpenChange={setShowDeleteConfirm}>
219
+ <AlertDialogContent>
220
+ <AlertDialogHeader>
221
+ <AlertDialogTitle className="flex items-center gap-2">
222
+ <AlertTriangle className="w-5 h-5 text-destructive" />
223
+ Delete Project Data?
224
+ </AlertDialogTitle>
225
+ <AlertDialogDescription>
226
+ This will move the project storage to trash.
227
+ <br />
228
+ <span className="text-muted-foreground text-sm">ID: {projectId}</span>
229
+ </AlertDialogDescription>
230
+ </AlertDialogHeader>
231
+ <AlertDialogFooter>
232
+ <AlertDialogCancel disabled={deleteMutation.isPending}>Cancel</AlertDialogCancel>
233
+ <AlertDialogAction
234
+ onClick={() => deleteMutation.mutate(projectId, { onSuccess: () => router.push('/') })}
235
+ disabled={deleteMutation.isPending}
236
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
237
+ >
238
+ {deleteMutation.isPending ? 'Deleting...' : 'Delete'}
239
+ </AlertDialogAction>
240
+ </AlertDialogFooter>
241
+ </AlertDialogContent>
242
+ </AlertDialog>
243
+ </div>
244
+ </div>
245
+ )
246
+ }
247
+
248
+ return (
249
+ <TerminalTabsProvider projectId={projectId}>
250
+ <ProjectPageContent projectId={projectId} project={project} />
251
+ </TerminalTabsProvider>
252
+ )
253
+ }