prjct-cli 0.10.14 → 0.11.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.
- package/CHANGELOG.md +19 -0
- package/bin/dev.js +217 -0
- package/bin/prjct +10 -0
- package/bin/serve.js +78 -0
- package/core/bus/index.js +322 -0
- package/core/command-registry.js +65 -0
- package/core/domain/snapshot-manager.js +375 -0
- package/core/plugin/hooks.js +313 -0
- package/core/plugin/index.js +52 -0
- package/core/plugin/loader.js +331 -0
- package/core/plugin/registry.js +325 -0
- package/core/plugins/webhook.js +143 -0
- package/core/session/index.js +449 -0
- package/core/session/metrics.js +293 -0
- package/package.json +28 -4
- package/packages/shared/dist/index.d.ts +615 -0
- package/packages/shared/dist/index.js +204 -0
- package/packages/shared/package.json +29 -0
- package/packages/shared/src/index.ts +9 -0
- package/packages/shared/src/schemas.ts +124 -0
- package/packages/shared/src/types.ts +187 -0
- package/packages/shared/src/utils.ts +148 -0
- package/packages/shared/tsconfig.json +18 -0
- package/packages/web/README.md +36 -0
- package/packages/web/app/api/claude/sessions/route.ts +44 -0
- package/packages/web/app/api/claude/status/route.ts +34 -0
- package/packages/web/app/api/projects/[id]/delete/route.ts +21 -0
- package/packages/web/app/api/projects/[id]/icon/route.ts +33 -0
- package/packages/web/app/api/projects/[id]/route.ts +29 -0
- package/packages/web/app/api/projects/[id]/stats/route.ts +36 -0
- package/packages/web/app/api/projects/[id]/status/route.ts +21 -0
- package/packages/web/app/api/projects/route.ts +16 -0
- package/packages/web/app/api/sessions/history/route.ts +122 -0
- package/packages/web/app/api/stats/route.ts +38 -0
- package/packages/web/app/error.tsx +34 -0
- package/packages/web/app/favicon.ico +0 -0
- package/packages/web/app/globals.css +155 -0
- package/packages/web/app/layout.tsx +43 -0
- package/packages/web/app/loading.tsx +7 -0
- package/packages/web/app/not-found.tsx +25 -0
- package/packages/web/app/page.tsx +227 -0
- package/packages/web/app/project/[id]/error.tsx +41 -0
- package/packages/web/app/project/[id]/loading.tsx +9 -0
- package/packages/web/app/project/[id]/not-found.tsx +27 -0
- package/packages/web/app/project/[id]/page.tsx +253 -0
- package/packages/web/app/project/[id]/stats/page.tsx +447 -0
- package/packages/web/app/sessions/page.tsx +165 -0
- package/packages/web/app/settings/page.tsx +150 -0
- package/packages/web/components/AppSidebar.tsx +113 -0
- package/packages/web/components/CommandButton.tsx +39 -0
- package/packages/web/components/ConnectionStatus.tsx +29 -0
- package/packages/web/components/Logo.tsx +65 -0
- package/packages/web/components/MarkdownContent.tsx +123 -0
- package/packages/web/components/ProjectAvatar.tsx +54 -0
- package/packages/web/components/TechStackBadges.tsx +20 -0
- package/packages/web/components/TerminalTab.tsx +84 -0
- package/packages/web/components/TerminalTabs.tsx +210 -0
- package/packages/web/components/charts/SessionsChart.tsx +172 -0
- package/packages/web/components/providers.tsx +45 -0
- package/packages/web/components/ui/alert-dialog.tsx +157 -0
- package/packages/web/components/ui/badge.tsx +46 -0
- package/packages/web/components/ui/button.tsx +60 -0
- package/packages/web/components/ui/card.tsx +92 -0
- package/packages/web/components/ui/chart.tsx +385 -0
- package/packages/web/components/ui/dropdown-menu.tsx +257 -0
- package/packages/web/components/ui/scroll-area.tsx +58 -0
- package/packages/web/components/ui/sheet.tsx +139 -0
- package/packages/web/components/ui/tabs.tsx +66 -0
- package/packages/web/components/ui/tooltip.tsx +61 -0
- package/packages/web/components.json +22 -0
- package/packages/web/context/TerminalContext.tsx +45 -0
- package/packages/web/context/TerminalTabsContext.tsx +136 -0
- package/packages/web/eslint.config.mjs +18 -0
- package/packages/web/hooks/useClaudeTerminal.ts +375 -0
- package/packages/web/hooks/useProjectStats.ts +38 -0
- package/packages/web/hooks/useProjects.ts +73 -0
- package/packages/web/hooks/useStats.ts +28 -0
- package/packages/web/lib/format.ts +23 -0
- package/packages/web/lib/parse-prjct-files.ts +1122 -0
- package/packages/web/lib/projects.ts +452 -0
- package/packages/web/lib/pty.ts +101 -0
- package/packages/web/lib/query-config.ts +44 -0
- package/packages/web/lib/utils.ts +6 -0
- package/packages/web/next-env.d.ts +6 -0
- package/packages/web/next.config.ts +7 -0
- package/packages/web/package.json +53 -0
- package/packages/web/postcss.config.mjs +7 -0
- package/packages/web/public/file.svg +1 -0
- package/packages/web/public/globe.svg +1 -0
- package/packages/web/public/next.svg +1 -0
- package/packages/web/public/vercel.svg +1 -0
- package/packages/web/public/window.svg +1 -0
- package/packages/web/server.ts +262 -0
- package/packages/web/tsconfig.json +34 -0
- package/templates/commands/done.md +176 -54
- package/templates/commands/history.md +176 -0
- package/templates/commands/init.md +28 -1
- package/templates/commands/now.md +191 -9
- package/templates/commands/pause.md +176 -12
- package/templates/commands/redo.md +142 -0
- package/templates/commands/resume.md +166 -62
- package/templates/commands/serve.md +121 -0
- package/templates/commands/ship.md +45 -1
- package/templates/commands/sync.md +34 -1
- package/templates/commands/undo.md +152 -0
|
@@ -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>"{projectToDelete?.name}"</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,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
|
+
}
|