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