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.
- package/bin/serve.js +90 -26
- package/package.json +11 -1
- 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
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useQuery } from '@tanstack/react-query'
|
|
4
|
+
import { useTheme } from 'next-themes'
|
|
5
|
+
import { Settings as SettingsIcon, CheckCircle, XCircle, Terminal, Sun, Moon, Monitor } from 'lucide-react'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
|
|
8
|
+
export default function Settings() {
|
|
9
|
+
const { theme, setTheme } = useTheme()
|
|
10
|
+
const { data: claudeStatus } = useQuery({
|
|
11
|
+
queryKey: ['claude-status'],
|
|
12
|
+
queryFn: async () => {
|
|
13
|
+
const res = await fetch('/api/claude/status')
|
|
14
|
+
const json = await res.json()
|
|
15
|
+
return json.data
|
|
16
|
+
}
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="p-6 h-full overflow-auto">
|
|
21
|
+
<header className="mb-6">
|
|
22
|
+
<h1 className="text-2xl font-bold flex items-center gap-2">
|
|
23
|
+
<SettingsIcon className="w-6 h-6" />
|
|
24
|
+
Settings
|
|
25
|
+
</h1>
|
|
26
|
+
</header>
|
|
27
|
+
|
|
28
|
+
<div className="space-y-6 max-w-2xl">
|
|
29
|
+
{/* Appearance */}
|
|
30
|
+
<section className="bg-card border border-border rounded-lg p-6">
|
|
31
|
+
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
32
|
+
<Sun className="w-5 h-5" />
|
|
33
|
+
Appearance
|
|
34
|
+
</h2>
|
|
35
|
+
|
|
36
|
+
<div className="space-y-4">
|
|
37
|
+
<div>
|
|
38
|
+
<p className="text-sm text-muted-foreground mb-3">
|
|
39
|
+
Choose how prjct looks to you. Select a single theme, or sync with your system settings.
|
|
40
|
+
</p>
|
|
41
|
+
<div className="flex gap-2">
|
|
42
|
+
<Button
|
|
43
|
+
variant={theme === 'light' ? 'default' : 'outline'}
|
|
44
|
+
size="sm"
|
|
45
|
+
onClick={() => setTheme('light')}
|
|
46
|
+
className="flex items-center gap-2"
|
|
47
|
+
>
|
|
48
|
+
<Sun className="w-4 h-4" />
|
|
49
|
+
Light
|
|
50
|
+
</Button>
|
|
51
|
+
<Button
|
|
52
|
+
variant={theme === 'dark' ? 'default' : 'outline'}
|
|
53
|
+
size="sm"
|
|
54
|
+
onClick={() => setTheme('dark')}
|
|
55
|
+
className="flex items-center gap-2"
|
|
56
|
+
>
|
|
57
|
+
<Moon className="w-4 h-4" />
|
|
58
|
+
Dark
|
|
59
|
+
</Button>
|
|
60
|
+
<Button
|
|
61
|
+
variant={theme === 'system' ? 'default' : 'outline'}
|
|
62
|
+
size="sm"
|
|
63
|
+
onClick={() => setTheme('system')}
|
|
64
|
+
className="flex items-center gap-2"
|
|
65
|
+
>
|
|
66
|
+
<Monitor className="w-4 h-4" />
|
|
67
|
+
System
|
|
68
|
+
</Button>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
</section>
|
|
73
|
+
|
|
74
|
+
{/* Claude Code Status */}
|
|
75
|
+
<section className="bg-card border border-border rounded-lg p-6">
|
|
76
|
+
<h2 className="text-lg font-semibold mb-4 flex items-center gap-2">
|
|
77
|
+
<Terminal className="w-5 h-5" />
|
|
78
|
+
Claude Code CLI
|
|
79
|
+
</h2>
|
|
80
|
+
|
|
81
|
+
<div className="flex items-center gap-3 mb-4">
|
|
82
|
+
{claudeStatus?.available ? (
|
|
83
|
+
<>
|
|
84
|
+
<CheckCircle className="w-5 h-5 text-green-500" />
|
|
85
|
+
<span className="text-green-500">Available</span>
|
|
86
|
+
</>
|
|
87
|
+
) : (
|
|
88
|
+
<>
|
|
89
|
+
<XCircle className="w-5 h-5 text-red-500" />
|
|
90
|
+
<span className="text-red-500">Not Found</span>
|
|
91
|
+
</>
|
|
92
|
+
)}
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{claudeStatus?.version && (
|
|
96
|
+
<p className="text-sm text-muted-foreground mb-4">
|
|
97
|
+
Version: {claudeStatus.version}
|
|
98
|
+
</p>
|
|
99
|
+
)}
|
|
100
|
+
|
|
101
|
+
<div className="bg-muted/50 rounded-md p-4 text-sm">
|
|
102
|
+
<p className="font-medium mb-2">How it works:</p>
|
|
103
|
+
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
|
|
104
|
+
<li>prjct uses Claude Code CLI via PTY (pseudo-terminal)</li>
|
|
105
|
+
<li>Uses your existing Claude subscription (no API costs)</li>
|
|
106
|
+
<li>Full Claude Code experience in your browser</li>
|
|
107
|
+
<li>All tools work: Read, Write, Bash, etc.</li>
|
|
108
|
+
</ul>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
{!claudeStatus?.available && (
|
|
112
|
+
<div className="mt-4 p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-md">
|
|
113
|
+
<p className="text-sm text-yellow-500">
|
|
114
|
+
Install Claude Code CLI from{' '}
|
|
115
|
+
<a
|
|
116
|
+
href="https://claude.ai/code"
|
|
117
|
+
target="_blank"
|
|
118
|
+
rel="noopener noreferrer"
|
|
119
|
+
className="underline"
|
|
120
|
+
>
|
|
121
|
+
claude.ai/code
|
|
122
|
+
</a>
|
|
123
|
+
</p>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
</section>
|
|
127
|
+
|
|
128
|
+
{/* About */}
|
|
129
|
+
<section className="bg-card border border-border rounded-lg p-6">
|
|
130
|
+
<h2 className="text-lg font-semibold mb-4">About prjct</h2>
|
|
131
|
+
|
|
132
|
+
<div className="space-y-2 text-sm text-muted-foreground">
|
|
133
|
+
<p>
|
|
134
|
+
<strong className="text-foreground">prjct</strong> is a developer momentum tool.
|
|
135
|
+
</p>
|
|
136
|
+
<p>
|
|
137
|
+
Track progress, ship features, stay focused. Built for Claude.
|
|
138
|
+
</p>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<div className="mt-4 pt-4 border-t border-border">
|
|
142
|
+
<p className="text-xs text-muted-foreground">
|
|
143
|
+
Version 0.1.0 • Made with prjct.app
|
|
144
|
+
</p>
|
|
145
|
+
</div>
|
|
146
|
+
</section>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
)
|
|
150
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import { usePathname } from 'next/navigation'
|
|
5
|
+
import {
|
|
6
|
+
LayoutDashboard,
|
|
7
|
+
Settings,
|
|
8
|
+
HelpCircle,
|
|
9
|
+
PanelLeft
|
|
10
|
+
} from 'lucide-react'
|
|
11
|
+
import { Logo } from './Logo'
|
|
12
|
+
import { cn } from '@/lib/utils'
|
|
13
|
+
import { useState } from 'react'
|
|
14
|
+
|
|
15
|
+
const navItems = [
|
|
16
|
+
{ href: '/', icon: LayoutDashboard, label: 'Dashboard' },
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
export function AppSidebar() {
|
|
20
|
+
const pathname = usePathname()
|
|
21
|
+
const [isCollapsed, setIsCollapsed] = useState(false)
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<aside
|
|
25
|
+
className={cn(
|
|
26
|
+
'flex h-full flex-col border-r border-border bg-card transition-all duration-200',
|
|
27
|
+
isCollapsed ? 'w-14' : 'w-60'
|
|
28
|
+
)}
|
|
29
|
+
>
|
|
30
|
+
{/* Header */}
|
|
31
|
+
<div className={cn(
|
|
32
|
+
'flex h-14 items-center border-b border-border',
|
|
33
|
+
isCollapsed ? 'justify-center' : 'justify-between px-3'
|
|
34
|
+
)}>
|
|
35
|
+
{!isCollapsed && (
|
|
36
|
+
<Link href="/">
|
|
37
|
+
<Logo size="xs" showText rounded />
|
|
38
|
+
</Link>
|
|
39
|
+
)}
|
|
40
|
+
<button
|
|
41
|
+
onClick={() => setIsCollapsed(!isCollapsed)}
|
|
42
|
+
className="h-8 w-8 rounded-md flex items-center justify-center text-muted-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
|
|
43
|
+
>
|
|
44
|
+
<PanelLeft className="h-4 w-4" />
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
{/* Nav */}
|
|
49
|
+
<nav className={cn(
|
|
50
|
+
'flex-1 overflow-y-auto py-4',
|
|
51
|
+
isCollapsed ? 'px-2' : 'px-3'
|
|
52
|
+
)}>
|
|
53
|
+
<div className="space-y-1">
|
|
54
|
+
{navItems.map(({ href, icon: Icon, label }) => {
|
|
55
|
+
const isActive = pathname === href || (href !== '/' && pathname.startsWith(href))
|
|
56
|
+
return (
|
|
57
|
+
<Link
|
|
58
|
+
key={href}
|
|
59
|
+
href={href}
|
|
60
|
+
className={cn(
|
|
61
|
+
'flex items-center gap-3 rounded-md px-3 py-2 transition-colors',
|
|
62
|
+
isCollapsed && 'justify-center px-2',
|
|
63
|
+
isActive
|
|
64
|
+
? 'bg-accent text-accent-foreground'
|
|
65
|
+
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
|
66
|
+
)}
|
|
67
|
+
title={isCollapsed ? label : undefined}
|
|
68
|
+
>
|
|
69
|
+
<Icon className="h-4 w-4 shrink-0" />
|
|
70
|
+
{!isCollapsed && <span>{label}</span>}
|
|
71
|
+
</Link>
|
|
72
|
+
)
|
|
73
|
+
})}
|
|
74
|
+
</div>
|
|
75
|
+
</nav>
|
|
76
|
+
|
|
77
|
+
{/* Footer */}
|
|
78
|
+
<div className={cn(
|
|
79
|
+
'border-t border-border p-3 space-y-1',
|
|
80
|
+
isCollapsed && 'px-2'
|
|
81
|
+
)}>
|
|
82
|
+
<Link
|
|
83
|
+
href="/settings"
|
|
84
|
+
className={cn(
|
|
85
|
+
'flex items-center gap-3 rounded-md px-3 py-2 transition-colors',
|
|
86
|
+
isCollapsed && 'justify-center px-2',
|
|
87
|
+
pathname === '/settings'
|
|
88
|
+
? 'bg-accent text-accent-foreground'
|
|
89
|
+
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
|
90
|
+
)}
|
|
91
|
+
title={isCollapsed ? 'Settings' : undefined}
|
|
92
|
+
>
|
|
93
|
+
<Settings className="h-4 w-4 shrink-0" />
|
|
94
|
+
{!isCollapsed && <span>Settings</span>}
|
|
95
|
+
</Link>
|
|
96
|
+
<Link
|
|
97
|
+
href="/help"
|
|
98
|
+
className={cn(
|
|
99
|
+
'flex items-center gap-3 rounded-md px-3 py-2 transition-colors',
|
|
100
|
+
isCollapsed && 'justify-center px-2',
|
|
101
|
+
pathname === '/help'
|
|
102
|
+
? 'bg-accent text-accent-foreground'
|
|
103
|
+
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
|
104
|
+
)}
|
|
105
|
+
title={isCollapsed ? 'Help' : undefined}
|
|
106
|
+
>
|
|
107
|
+
<HelpCircle className="h-4 w-4 shrink-0" />
|
|
108
|
+
{!isCollapsed && <span>Need help?</span>}
|
|
109
|
+
</Link>
|
|
110
|
+
</div>
|
|
111
|
+
</aside>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { LucideIcon } from 'lucide-react'
|
|
4
|
+
import { Button } from '@/components/ui/button'
|
|
5
|
+
import {
|
|
6
|
+
Tooltip,
|
|
7
|
+
TooltipContent,
|
|
8
|
+
TooltipTrigger,
|
|
9
|
+
} from '@/components/ui/tooltip'
|
|
10
|
+
|
|
11
|
+
interface CommandButtonProps {
|
|
12
|
+
cmd: string
|
|
13
|
+
icon: LucideIcon
|
|
14
|
+
tip: string
|
|
15
|
+
disabled?: boolean
|
|
16
|
+
onClick: () => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function CommandButton({ cmd, icon: Icon, tip, disabled, onClick }: CommandButtonProps) {
|
|
20
|
+
return (
|
|
21
|
+
<Tooltip>
|
|
22
|
+
<TooltipTrigger asChild>
|
|
23
|
+
<Button
|
|
24
|
+
variant="ghost"
|
|
25
|
+
size="icon"
|
|
26
|
+
onClick={onClick}
|
|
27
|
+
disabled={disabled}
|
|
28
|
+
className="h-11 w-11"
|
|
29
|
+
>
|
|
30
|
+
<Icon className="w-5 h-5" />
|
|
31
|
+
</Button>
|
|
32
|
+
</TooltipTrigger>
|
|
33
|
+
<TooltipContent side="right">
|
|
34
|
+
<p className="font-semibold">{tip}</p>
|
|
35
|
+
<p className="font-mono text-xs text-muted-foreground">{cmd}</p>
|
|
36
|
+
</TooltipContent>
|
|
37
|
+
</Tooltip>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Badge } from '@/components/ui/badge'
|
|
2
|
+
import { RefreshCw } from 'lucide-react'
|
|
3
|
+
|
|
4
|
+
interface ConnectionStatusProps {
|
|
5
|
+
isConnected: boolean
|
|
6
|
+
isReconnecting?: boolean
|
|
7
|
+
reconnectInfo?: { attempt: number; max: number } | null
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function ConnectionStatus({ isConnected, isReconnecting, reconnectInfo }: ConnectionStatusProps) {
|
|
11
|
+
if (isReconnecting && reconnectInfo) {
|
|
12
|
+
return (
|
|
13
|
+
<Badge variant="outline" className="text-yellow-500 border-yellow-500/50 animate-pulse">
|
|
14
|
+
<RefreshCw className="w-3 h-3 mr-1 animate-spin" />
|
|
15
|
+
Reconnecting ({reconnectInfo.attempt}/{reconnectInfo.max})
|
|
16
|
+
</Badge>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (isConnected) {
|
|
21
|
+
return (
|
|
22
|
+
<Badge variant="outline" className="text-green-500 border-green-500/50">
|
|
23
|
+
Connected
|
|
24
|
+
</Badge>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils'
|
|
2
|
+
|
|
3
|
+
interface LogoProps {
|
|
4
|
+
showText?: boolean
|
|
5
|
+
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
|
|
6
|
+
rounded?: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function Logo({ showText = true, size = 'md', rounded = false }: LogoProps) {
|
|
10
|
+
const logoSize = {
|
|
11
|
+
xs: 'size-7',
|
|
12
|
+
sm: 'size-10',
|
|
13
|
+
md: 'size-12',
|
|
14
|
+
lg: 'size-14',
|
|
15
|
+
xl: 'size-16',
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const textSize = {
|
|
19
|
+
xs: 'text-sm',
|
|
20
|
+
sm: 'text-lg',
|
|
21
|
+
md: 'text-lg',
|
|
22
|
+
lg: 'text-2xl',
|
|
23
|
+
xl: 'text-3xl',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const containerTextSize = {
|
|
27
|
+
xs: 'text-sm',
|
|
28
|
+
sm: 'text-base',
|
|
29
|
+
md: 'text-xl',
|
|
30
|
+
lg: 'text-2xl',
|
|
31
|
+
xl: 'text-3xl',
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const brandTextSize = {
|
|
35
|
+
xs: 'text-xs',
|
|
36
|
+
sm: 'text-sm',
|
|
37
|
+
md: 'text-lg',
|
|
38
|
+
lg: 'text-xl',
|
|
39
|
+
xl: 'text-2xl',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
className={cn('flex items-center gap-2', containerTextSize[size])}
|
|
45
|
+
data-testid="prjct-logo"
|
|
46
|
+
>
|
|
47
|
+
<div className="relative isolate overflow-visible">
|
|
48
|
+
<div className={cn("fancy-border pointer-events-none", rounded && "rounded-full")}></div>
|
|
49
|
+
<div
|
|
50
|
+
className={cn(
|
|
51
|
+
'relative z-10 flex items-center justify-center border border-border bg-foreground text-background shadow-sm',
|
|
52
|
+
rounded ? 'rounded-full' : 'rounded-lg',
|
|
53
|
+
logoSize[size]
|
|
54
|
+
)}
|
|
55
|
+
data-testid="prjct-logo-icon"
|
|
56
|
+
>
|
|
57
|
+
<p className={cn('mb-0.5 inline-block font-bold leading-none', textSize[size])}>p/</p>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
{showText && (
|
|
61
|
+
<span className={cn(brandTextSize[size], 'font-bold text-foreground')}>prjct</span>
|
|
62
|
+
)}
|
|
63
|
+
</div>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import ReactMarkdown from 'react-markdown'
|
|
4
|
+
import remarkGfm from 'remark-gfm'
|
|
5
|
+
import { cn } from '@/lib/utils'
|
|
6
|
+
|
|
7
|
+
interface MarkdownContentProps {
|
|
8
|
+
content: string
|
|
9
|
+
className?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function MarkdownContent({ content, className }: MarkdownContentProps) {
|
|
13
|
+
if (!content || !content.trim()) {
|
|
14
|
+
return (
|
|
15
|
+
<p className="text-sm text-muted-foreground italic">No content</p>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className={cn('prose prose-sm dark:prose-invert max-w-none', className)}>
|
|
21
|
+
<ReactMarkdown
|
|
22
|
+
remarkPlugins={[remarkGfm]}
|
|
23
|
+
components={{
|
|
24
|
+
// Headers
|
|
25
|
+
h1: ({ children }) => (
|
|
26
|
+
<h1 className="text-xl font-bold mt-4 mb-2 first:mt-0">{children}</h1>
|
|
27
|
+
),
|
|
28
|
+
h2: ({ children }) => (
|
|
29
|
+
<h2 className="text-lg font-semibold mt-4 mb-2 border-b pb-1">{children}</h2>
|
|
30
|
+
),
|
|
31
|
+
h3: ({ children }) => (
|
|
32
|
+
<h3 className="text-base font-medium mt-3 mb-1">{children}</h3>
|
|
33
|
+
),
|
|
34
|
+
// Lists
|
|
35
|
+
ul: ({ children }) => (
|
|
36
|
+
<ul className="list-disc list-inside space-y-1 my-2">{children}</ul>
|
|
37
|
+
),
|
|
38
|
+
ol: ({ children }) => (
|
|
39
|
+
<ol className="list-decimal list-inside space-y-1 my-2">{children}</ol>
|
|
40
|
+
),
|
|
41
|
+
li: ({ children }) => (
|
|
42
|
+
<li className="text-sm">{children}</li>
|
|
43
|
+
),
|
|
44
|
+
// Checkbox lists (GFM)
|
|
45
|
+
input: ({ checked, ...props }) => (
|
|
46
|
+
<input
|
|
47
|
+
type="checkbox"
|
|
48
|
+
checked={checked}
|
|
49
|
+
readOnly
|
|
50
|
+
className="mr-2 rounded"
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
),
|
|
54
|
+
// Paragraphs
|
|
55
|
+
p: ({ children }) => (
|
|
56
|
+
<p className="text-sm my-1.5 leading-relaxed">{children}</p>
|
|
57
|
+
),
|
|
58
|
+
// Code
|
|
59
|
+
code: ({ className, children, ...props }) => {
|
|
60
|
+
const isInline = !className
|
|
61
|
+
if (isInline) {
|
|
62
|
+
return (
|
|
63
|
+
<code className="bg-muted px-1 py-0.5 rounded text-xs font-mono" {...props}>
|
|
64
|
+
{children}
|
|
65
|
+
</code>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
return (
|
|
69
|
+
<code className={cn('block bg-muted p-2 rounded text-xs font-mono overflow-x-auto', className)} {...props}>
|
|
70
|
+
{children}
|
|
71
|
+
</code>
|
|
72
|
+
)
|
|
73
|
+
},
|
|
74
|
+
pre: ({ children }) => (
|
|
75
|
+
<pre className="bg-muted p-3 rounded-md overflow-x-auto my-2 text-xs">
|
|
76
|
+
{children}
|
|
77
|
+
</pre>
|
|
78
|
+
),
|
|
79
|
+
// Tables
|
|
80
|
+
table: ({ children }) => (
|
|
81
|
+
<div className="overflow-x-auto my-2">
|
|
82
|
+
<table className="min-w-full text-xs border-collapse">{children}</table>
|
|
83
|
+
</div>
|
|
84
|
+
),
|
|
85
|
+
thead: ({ children }) => (
|
|
86
|
+
<thead className="bg-muted">{children}</thead>
|
|
87
|
+
),
|
|
88
|
+
th: ({ children }) => (
|
|
89
|
+
<th className="px-2 py-1 text-left font-medium border-b">{children}</th>
|
|
90
|
+
),
|
|
91
|
+
td: ({ children }) => (
|
|
92
|
+
<td className="px-2 py-1 border-b">{children}</td>
|
|
93
|
+
),
|
|
94
|
+
// Blockquotes
|
|
95
|
+
blockquote: ({ children }) => (
|
|
96
|
+
<blockquote className="border-l-2 border-muted-foreground/30 pl-3 italic text-muted-foreground my-2">
|
|
97
|
+
{children}
|
|
98
|
+
</blockquote>
|
|
99
|
+
),
|
|
100
|
+
// Horizontal rules
|
|
101
|
+
hr: () => (
|
|
102
|
+
<hr className="my-4 border-border" />
|
|
103
|
+
),
|
|
104
|
+
// Strong/em
|
|
105
|
+
strong: ({ children }) => (
|
|
106
|
+
<strong className="font-semibold">{children}</strong>
|
|
107
|
+
),
|
|
108
|
+
em: ({ children }) => (
|
|
109
|
+
<em className="italic">{children}</em>
|
|
110
|
+
),
|
|
111
|
+
// Links
|
|
112
|
+
a: ({ href, children }) => (
|
|
113
|
+
<a href={href} className="text-primary hover:underline" target="_blank" rel="noopener noreferrer">
|
|
114
|
+
{children}
|
|
115
|
+
</a>
|
|
116
|
+
),
|
|
117
|
+
}}
|
|
118
|
+
>
|
|
119
|
+
{content}
|
|
120
|
+
</ReactMarkdown>
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/lib/utils'
|
|
4
|
+
|
|
5
|
+
interface ProjectAvatarProps {
|
|
6
|
+
projectId: string
|
|
7
|
+
name: string
|
|
8
|
+
iconPath?: string | null
|
|
9
|
+
size?: 'sm' | 'md' | 'lg'
|
|
10
|
+
className?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const sizeClasses = {
|
|
14
|
+
sm: 'w-8 h-8 text-xs',
|
|
15
|
+
md: 'w-9 h-9 text-sm',
|
|
16
|
+
lg: 'w-10 h-10 text-sm'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ProjectAvatar({
|
|
20
|
+
projectId,
|
|
21
|
+
name,
|
|
22
|
+
iconPath,
|
|
23
|
+
size = 'md',
|
|
24
|
+
className
|
|
25
|
+
}: ProjectAvatarProps) {
|
|
26
|
+
const initials = name.slice(0, 2).toUpperCase()
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div
|
|
30
|
+
className={cn(
|
|
31
|
+
'rounded-lg bg-muted flex items-center justify-center overflow-hidden shrink-0',
|
|
32
|
+
sizeClasses[size],
|
|
33
|
+
className
|
|
34
|
+
)}
|
|
35
|
+
>
|
|
36
|
+
{iconPath ? (
|
|
37
|
+
<img
|
|
38
|
+
src={`/api/projects/${projectId}/icon`}
|
|
39
|
+
alt=""
|
|
40
|
+
className="w-full h-full object-cover"
|
|
41
|
+
onError={(e) => {
|
|
42
|
+
e.currentTarget.style.display = 'none'
|
|
43
|
+
if (e.currentTarget.nextElementSibling) {
|
|
44
|
+
(e.currentTarget.nextElementSibling as HTMLElement).classList.remove('hidden')
|
|
45
|
+
}
|
|
46
|
+
}}
|
|
47
|
+
/>
|
|
48
|
+
) : null}
|
|
49
|
+
<span className={cn('font-bold text-muted-foreground', iconPath ? 'hidden' : '')}>
|
|
50
|
+
{initials}
|
|
51
|
+
</span>
|
|
52
|
+
</div>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Badge } from '@/components/ui/badge'
|
|
2
|
+
|
|
3
|
+
interface TechStackBadgesProps {
|
|
4
|
+
techStack: string[]
|
|
5
|
+
max?: number
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function TechStackBadges({ techStack, max = 4 }: TechStackBadgesProps) {
|
|
9
|
+
if (!techStack || techStack.length === 0) return null
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<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">
|
|
15
|
+
{tech}
|
|
16
|
+
</Badge>
|
|
17
|
+
))}
|
|
18
|
+
</div>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useCallback } from 'react'
|
|
4
|
+
import { useClaudeTerminal } from '@/hooks/useClaudeTerminal'
|
|
5
|
+
import { useTerminalTabs, type TerminalSession } from '@/context/TerminalTabsContext'
|
|
6
|
+
|
|
7
|
+
interface TerminalTabProps {
|
|
8
|
+
session: TerminalSession
|
|
9
|
+
projectDir: string
|
|
10
|
+
isActive: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function TerminalTab({ session, projectDir, isActive }: TerminalTabProps) {
|
|
14
|
+
const containerRef = useRef<HTMLDivElement>(null)
|
|
15
|
+
const hasInitializedRef = useRef(false)
|
|
16
|
+
const hasConnectedRef = useRef(false)
|
|
17
|
+
const { updateSession, registerSendInput, registerFocusTerminal } = useTerminalTabs()
|
|
18
|
+
|
|
19
|
+
const handleConnect = useCallback(() => {
|
|
20
|
+
updateSession(session.id, { isConnected: true, isLoading: false })
|
|
21
|
+
}, [session.id, updateSession])
|
|
22
|
+
|
|
23
|
+
const handleDisconnect = useCallback(() => {
|
|
24
|
+
updateSession(session.id, { isConnected: false, isLoading: false })
|
|
25
|
+
}, [session.id, updateSession])
|
|
26
|
+
|
|
27
|
+
const handleError = useCallback((error: string) => {
|
|
28
|
+
console.error(`[Terminal ${session.id}] Error:`, error)
|
|
29
|
+
updateSession(session.id, { isLoading: false })
|
|
30
|
+
}, [session.id, updateSession])
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
initTerminal,
|
|
34
|
+
connect,
|
|
35
|
+
disconnect,
|
|
36
|
+
sendInput,
|
|
37
|
+
focusTerminal,
|
|
38
|
+
} = useClaudeTerminal({
|
|
39
|
+
sessionId: session.id,
|
|
40
|
+
projectDir,
|
|
41
|
+
onConnect: handleConnect,
|
|
42
|
+
onDisconnect: handleDisconnect,
|
|
43
|
+
onError: handleError,
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
// Initialize terminal AND connect - only once
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (containerRef.current && !hasInitializedRef.current) {
|
|
49
|
+
hasInitializedRef.current = true
|
|
50
|
+
|
|
51
|
+
initTerminal(containerRef.current).then(() => {
|
|
52
|
+
if (!hasConnectedRef.current) {
|
|
53
|
+
hasConnectedRef.current = true
|
|
54
|
+
connect()
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
}, []) // Empty deps - run only on mount
|
|
59
|
+
|
|
60
|
+
// Register sendInput and focusTerminal for this session
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
registerSendInput(session.id, sendInput)
|
|
63
|
+
registerFocusTerminal(session.id, focusTerminal)
|
|
64
|
+
}, [session.id, sendInput, focusTerminal, registerSendInput, registerFocusTerminal])
|
|
65
|
+
|
|
66
|
+
// Expose disconnect for external use
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
// Store disconnect function on window for access from parent
|
|
69
|
+
const key = `terminal_disconnect_${session.id}`
|
|
70
|
+
;(window as unknown as Record<string, () => void>)[key] = disconnect
|
|
71
|
+
return () => {
|
|
72
|
+
delete (window as unknown as Record<string, () => void>)[key]
|
|
73
|
+
}
|
|
74
|
+
}, [session.id, disconnect])
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
className="absolute inset-0 bg-[#0a0a0f] px-2 py-2"
|
|
79
|
+
style={{ display: isActive ? 'block' : 'none' }}
|
|
80
|
+
>
|
|
81
|
+
<div ref={containerRef} className="h-full w-full" />
|
|
82
|
+
</div>
|
|
83
|
+
)
|
|
84
|
+
}
|