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.
Files changed (105) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/bin/dev.js +217 -0
  3. package/bin/prjct +10 -0
  4. package/bin/serve.js +78 -0
  5. package/core/bus/index.js +322 -0
  6. package/core/command-registry.js +65 -0
  7. package/core/domain/snapshot-manager.js +375 -0
  8. package/core/plugin/hooks.js +313 -0
  9. package/core/plugin/index.js +52 -0
  10. package/core/plugin/loader.js +331 -0
  11. package/core/plugin/registry.js +325 -0
  12. package/core/plugins/webhook.js +143 -0
  13. package/core/session/index.js +449 -0
  14. package/core/session/metrics.js +293 -0
  15. package/package.json +28 -4
  16. package/packages/shared/dist/index.d.ts +615 -0
  17. package/packages/shared/dist/index.js +204 -0
  18. package/packages/shared/package.json +29 -0
  19. package/packages/shared/src/index.ts +9 -0
  20. package/packages/shared/src/schemas.ts +124 -0
  21. package/packages/shared/src/types.ts +187 -0
  22. package/packages/shared/src/utils.ts +148 -0
  23. package/packages/shared/tsconfig.json +18 -0
  24. package/packages/web/README.md +36 -0
  25. package/packages/web/app/api/claude/sessions/route.ts +44 -0
  26. package/packages/web/app/api/claude/status/route.ts +34 -0
  27. package/packages/web/app/api/projects/[id]/delete/route.ts +21 -0
  28. package/packages/web/app/api/projects/[id]/icon/route.ts +33 -0
  29. package/packages/web/app/api/projects/[id]/route.ts +29 -0
  30. package/packages/web/app/api/projects/[id]/stats/route.ts +36 -0
  31. package/packages/web/app/api/projects/[id]/status/route.ts +21 -0
  32. package/packages/web/app/api/projects/route.ts +16 -0
  33. package/packages/web/app/api/sessions/history/route.ts +122 -0
  34. package/packages/web/app/api/stats/route.ts +38 -0
  35. package/packages/web/app/error.tsx +34 -0
  36. package/packages/web/app/favicon.ico +0 -0
  37. package/packages/web/app/globals.css +155 -0
  38. package/packages/web/app/layout.tsx +43 -0
  39. package/packages/web/app/loading.tsx +7 -0
  40. package/packages/web/app/not-found.tsx +25 -0
  41. package/packages/web/app/page.tsx +227 -0
  42. package/packages/web/app/project/[id]/error.tsx +41 -0
  43. package/packages/web/app/project/[id]/loading.tsx +9 -0
  44. package/packages/web/app/project/[id]/not-found.tsx +27 -0
  45. package/packages/web/app/project/[id]/page.tsx +253 -0
  46. package/packages/web/app/project/[id]/stats/page.tsx +447 -0
  47. package/packages/web/app/sessions/page.tsx +165 -0
  48. package/packages/web/app/settings/page.tsx +150 -0
  49. package/packages/web/components/AppSidebar.tsx +113 -0
  50. package/packages/web/components/CommandButton.tsx +39 -0
  51. package/packages/web/components/ConnectionStatus.tsx +29 -0
  52. package/packages/web/components/Logo.tsx +65 -0
  53. package/packages/web/components/MarkdownContent.tsx +123 -0
  54. package/packages/web/components/ProjectAvatar.tsx +54 -0
  55. package/packages/web/components/TechStackBadges.tsx +20 -0
  56. package/packages/web/components/TerminalTab.tsx +84 -0
  57. package/packages/web/components/TerminalTabs.tsx +210 -0
  58. package/packages/web/components/charts/SessionsChart.tsx +172 -0
  59. package/packages/web/components/providers.tsx +45 -0
  60. package/packages/web/components/ui/alert-dialog.tsx +157 -0
  61. package/packages/web/components/ui/badge.tsx +46 -0
  62. package/packages/web/components/ui/button.tsx +60 -0
  63. package/packages/web/components/ui/card.tsx +92 -0
  64. package/packages/web/components/ui/chart.tsx +385 -0
  65. package/packages/web/components/ui/dropdown-menu.tsx +257 -0
  66. package/packages/web/components/ui/scroll-area.tsx +58 -0
  67. package/packages/web/components/ui/sheet.tsx +139 -0
  68. package/packages/web/components/ui/tabs.tsx +66 -0
  69. package/packages/web/components/ui/tooltip.tsx +61 -0
  70. package/packages/web/components.json +22 -0
  71. package/packages/web/context/TerminalContext.tsx +45 -0
  72. package/packages/web/context/TerminalTabsContext.tsx +136 -0
  73. package/packages/web/eslint.config.mjs +18 -0
  74. package/packages/web/hooks/useClaudeTerminal.ts +375 -0
  75. package/packages/web/hooks/useProjectStats.ts +38 -0
  76. package/packages/web/hooks/useProjects.ts +73 -0
  77. package/packages/web/hooks/useStats.ts +28 -0
  78. package/packages/web/lib/format.ts +23 -0
  79. package/packages/web/lib/parse-prjct-files.ts +1122 -0
  80. package/packages/web/lib/projects.ts +452 -0
  81. package/packages/web/lib/pty.ts +101 -0
  82. package/packages/web/lib/query-config.ts +44 -0
  83. package/packages/web/lib/utils.ts +6 -0
  84. package/packages/web/next-env.d.ts +6 -0
  85. package/packages/web/next.config.ts +7 -0
  86. package/packages/web/package.json +53 -0
  87. package/packages/web/postcss.config.mjs +7 -0
  88. package/packages/web/public/file.svg +1 -0
  89. package/packages/web/public/globe.svg +1 -0
  90. package/packages/web/public/next.svg +1 -0
  91. package/packages/web/public/vercel.svg +1 -0
  92. package/packages/web/public/window.svg +1 -0
  93. package/packages/web/server.ts +262 -0
  94. package/packages/web/tsconfig.json +34 -0
  95. package/templates/commands/done.md +176 -54
  96. package/templates/commands/history.md +176 -0
  97. package/templates/commands/init.md +28 -1
  98. package/templates/commands/now.md +191 -9
  99. package/templates/commands/pause.md +176 -12
  100. package/templates/commands/redo.md +142 -0
  101. package/templates/commands/resume.md +166 -62
  102. package/templates/commands/serve.md +121 -0
  103. package/templates/commands/ship.md +45 -1
  104. package/templates/commands/sync.md +34 -1
  105. package/templates/commands/undo.md +152 -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
+ }