prjct-cli 0.13.3 → 0.15.0
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 +106 -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/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/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,59 @@
|
|
|
1
|
+
import { notFound } from 'next/navigation'
|
|
2
|
+
import Link from 'next/link'
|
|
3
|
+
import type { Metadata } from 'next'
|
|
4
|
+
import { ArrowLeft } from 'lucide-react'
|
|
5
|
+
import { getStats } from '@/lib/services/stats.server'
|
|
6
|
+
import { getProject } from '@/lib/services/projects.server'
|
|
7
|
+
import { getProjectEmoji } from '@/lib/project-colors'
|
|
8
|
+
import { WeeklyReports } from '@/components/WeeklyReports'
|
|
9
|
+
|
|
10
|
+
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
|
|
11
|
+
const { id: projectId } = await params
|
|
12
|
+
const project = await getProject(projectId)
|
|
13
|
+
const projectName = project?.name ?? projectId
|
|
14
|
+
const emoji = getProjectEmoji(projectId)
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
title: `${emoji} ${projectName} / Reports / p.`,
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface PageProps {
|
|
22
|
+
params: Promise<{ id: string }>
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default async function ReportsPage({ params }: PageProps) {
|
|
26
|
+
const { id: projectId } = await params
|
|
27
|
+
|
|
28
|
+
const [project, stats] = await Promise.all([
|
|
29
|
+
getProject(projectId),
|
|
30
|
+
getStats(projectId)
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
if (!stats.hasData) {
|
|
34
|
+
notFound()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const projectName = project?.name ?? projectId
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className="flex h-full flex-col p-4 md:p-6 overflow-auto">
|
|
41
|
+
{/* Header with back navigation */}
|
|
42
|
+
<div className="pl-10 md:pl-0 mb-6">
|
|
43
|
+
<Link
|
|
44
|
+
href={`/project/${projectId}`}
|
|
45
|
+
className="inline-flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors mb-4"
|
|
46
|
+
>
|
|
47
|
+
<ArrowLeft className="h-4 w-4" />
|
|
48
|
+
Back to {projectName}
|
|
49
|
+
</Link>
|
|
50
|
+
<h1 className="text-3xl font-bold">Weekly Reports</h1>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div className="pl-10 md:pl-0">
|
|
54
|
+
<WeeklyReports stats={stats} projectName={projectName} />
|
|
55
|
+
</div>
|
|
56
|
+
<div className="h-4" />
|
|
57
|
+
</div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Metadata } from 'next'
|
|
2
|
+
import { getStats } from '@/lib/services/stats.server'
|
|
3
|
+
import { getProject } from '@/lib/projects'
|
|
4
|
+
import { getProjectEmoji } from '@/lib/project-colors'
|
|
5
|
+
import { PrintableReport } from '@/components/WeeklyReports/PrintableReport'
|
|
6
|
+
|
|
7
|
+
export async function generateMetadata({ params }: { params: Promise<{ id: string }> }): Promise<Metadata> {
|
|
8
|
+
const { id: projectId } = await params
|
|
9
|
+
const project = await getProject(projectId)
|
|
10
|
+
const projectName = project?.name ?? projectId
|
|
11
|
+
const emoji = getProjectEmoji(projectId)
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
title: `${emoji} ${projectName} / Print / p.`,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
params: Promise<{ id: string }>
|
|
20
|
+
searchParams: Promise<{ weeks?: string; year?: string }>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default async function PrintReportPage({ params, searchParams }: Props) {
|
|
24
|
+
const { id } = await params
|
|
25
|
+
const { weeks, year } = await searchParams
|
|
26
|
+
|
|
27
|
+
const project = await getProject(id)
|
|
28
|
+
if (!project) {
|
|
29
|
+
return <div className="p-8 text-center">Project not found</div>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const stats = await getStats(id)
|
|
33
|
+
|
|
34
|
+
// Parse weeks from query params (comma-separated)
|
|
35
|
+
const selectedWeeks = weeks
|
|
36
|
+
? weeks.split(',').map(Number).filter(n => !isNaN(n))
|
|
37
|
+
: [getCurrentWeek()]
|
|
38
|
+
|
|
39
|
+
const selectedYear = year ? parseInt(year) : new Date().getFullYear()
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<PrintableReport
|
|
43
|
+
stats={stats}
|
|
44
|
+
projectName={project.name}
|
|
45
|
+
selectedWeeks={selectedWeeks}
|
|
46
|
+
year={selectedYear}
|
|
47
|
+
/>
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getCurrentWeek(): number {
|
|
52
|
+
const now = new Date()
|
|
53
|
+
const d = new Date(Date.UTC(now.getFullYear(), now.getMonth(), now.getDate()))
|
|
54
|
+
const dayNum = d.getUTCDay() || 7
|
|
55
|
+
d.setUTCDate(d.getUTCDate() + 4 - dayNum)
|
|
56
|
+
const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1))
|
|
57
|
+
return Math.ceil((((d.getTime() - yearStart.getTime()) / 86400000) + 1) / 7)
|
|
58
|
+
}
|
|
@@ -1,63 +1,93 @@
|
|
|
1
|
-
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
2
4
|
import { EmptyState } from '@/components/EmptyState'
|
|
3
|
-
import {
|
|
5
|
+
import { ExpandButton } from '@/components/ExpandButton'
|
|
6
|
+
import { Bot, Star, TrendingUp, Wrench } from 'lucide-react'
|
|
4
7
|
import { Badge } from '@/components/ui/badge'
|
|
5
8
|
import { cn } from '@/lib/utils'
|
|
6
9
|
import type { AgentsCardProps } from './AgentsCard.types'
|
|
7
10
|
|
|
8
|
-
|
|
9
|
-
|
|
11
|
+
const COLLAPSED_LIMIT = 12
|
|
12
|
+
|
|
13
|
+
export function AgentsCard({ agents, codeHref, className }: AgentsCardProps) {
|
|
14
|
+
const [expanded, setExpanded] = useState(false)
|
|
15
|
+
const displayAgents = expanded ? agents : agents.slice(0, COLLAPSED_LIMIT)
|
|
16
|
+
const hasMore = agents.length > COLLAPSED_LIMIT
|
|
10
17
|
|
|
11
18
|
return (
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
<div className={cn(
|
|
20
|
+
'relative overflow-hidden rounded-xl border bg-card p-4',
|
|
21
|
+
className
|
|
22
|
+
)}>
|
|
23
|
+
<div className="flex items-center justify-between mb-3">
|
|
24
|
+
<div className="flex items-center gap-2">
|
|
25
|
+
<Bot className="h-4 w-4 text-muted-foreground" />
|
|
26
|
+
<span className="text-xs font-bold uppercase tracking-[0.15em] text-muted-foreground">
|
|
27
|
+
Agents
|
|
28
|
+
</span>
|
|
29
|
+
</div>
|
|
30
|
+
<span className="text-xs font-medium text-muted-foreground tabular-nums">
|
|
31
|
+
{agents.length} available
|
|
32
|
+
</span>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
19
35
|
{agents.length === 0 ? (
|
|
20
36
|
<EmptyState
|
|
21
37
|
icon={Bot}
|
|
22
38
|
title="No agents"
|
|
23
39
|
description="Run /p:sync to generate"
|
|
40
|
+
command="/p:sync"
|
|
41
|
+
href={codeHref}
|
|
24
42
|
compact
|
|
25
43
|
/>
|
|
26
44
|
) : (
|
|
27
|
-
<div className="
|
|
45
|
+
<div className="space-y-2">
|
|
28
46
|
{displayAgents.map((agent) => {
|
|
29
47
|
const hasPerformance = agent.successRate !== undefined
|
|
30
48
|
const isTopPerformer = hasPerformance && agent.successRate! >= 80
|
|
31
49
|
|
|
32
50
|
return (
|
|
33
|
-
<
|
|
51
|
+
<div
|
|
34
52
|
key={agent.name}
|
|
35
|
-
|
|
36
|
-
className={cn(
|
|
37
|
-
"text-xs px-2 py-0.5 font-mono inline-flex items-center gap-1",
|
|
38
|
-
isTopPerformer && "bg-emerald-500/10 text-emerald-600 dark:text-emerald-400 border-emerald-500/20"
|
|
39
|
-
)}
|
|
53
|
+
className="flex items-center justify-between gap-2 py-1.5 px-2 rounded-lg -mx-2 hover:bg-muted/50"
|
|
40
54
|
>
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
55
|
+
<div className="flex items-center gap-2 min-w-0">
|
|
56
|
+
{isTopPerformer ? (
|
|
57
|
+
<Star className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
58
|
+
) : (
|
|
59
|
+
<Wrench className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
|
|
60
|
+
)}
|
|
61
|
+
<span className="font-mono text-sm truncate">@{agent.name}</span>
|
|
62
|
+
</div>
|
|
63
|
+
<div className="flex items-center gap-2 shrink-0">
|
|
64
|
+
{hasPerformance && (
|
|
65
|
+
<span className="text-xs font-medium tabular-nums text-muted-foreground">
|
|
66
|
+
{agent.successRate}%
|
|
67
|
+
</span>
|
|
68
|
+
)}
|
|
69
|
+
{agent.improving && (
|
|
70
|
+
<TrendingUp className="h-3 w-3 text-muted-foreground" />
|
|
71
|
+
)}
|
|
72
|
+
{agent.tasksCompleted !== undefined && agent.tasksCompleted > 0 && (
|
|
73
|
+
<Badge variant="secondary" className="text-xs px-1.5 py-0 h-4">
|
|
74
|
+
{agent.tasksCompleted} tasks
|
|
75
|
+
</Badge>
|
|
76
|
+
)}
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
52
79
|
)
|
|
53
80
|
})}
|
|
54
|
-
{
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
81
|
+
{hasMore && (
|
|
82
|
+
<ExpandButton
|
|
83
|
+
expanded={expanded}
|
|
84
|
+
totalCount={agents.length}
|
|
85
|
+
collapsedLimit={COLLAPSED_LIMIT}
|
|
86
|
+
onToggle={() => setExpanded(!expanded)}
|
|
87
|
+
/>
|
|
58
88
|
)}
|
|
59
89
|
</div>
|
|
60
90
|
)}
|
|
61
|
-
</
|
|
91
|
+
</div>
|
|
62
92
|
)
|
|
63
93
|
}
|
|
@@ -8,7 +8,11 @@ import {
|
|
|
8
8
|
HelpCircle,
|
|
9
9
|
Menu,
|
|
10
10
|
PanelLeft,
|
|
11
|
+
Terminal,
|
|
11
12
|
} from 'lucide-react'
|
|
13
|
+
import { useGlobalTerminal } from '@/context/GlobalTerminalContext'
|
|
14
|
+
import { Badge } from '@/components/ui/badge'
|
|
15
|
+
import { ProjectSelectorModal } from '@/components/ProjectSelectorModal'
|
|
12
16
|
import { Logo } from '@/components/Logo'
|
|
13
17
|
import { cn } from '@/lib/utils'
|
|
14
18
|
import { useState, useEffect } from 'react'
|
|
@@ -22,11 +26,15 @@ const navItems = [
|
|
|
22
26
|
function SidebarContent({
|
|
23
27
|
onNavigate,
|
|
24
28
|
isCollapsed = false,
|
|
25
|
-
onToggleCollapse
|
|
29
|
+
onToggleCollapse,
|
|
30
|
+
onTerminalClick,
|
|
31
|
+
sessionCount
|
|
26
32
|
}: {
|
|
27
33
|
onNavigate?: () => void
|
|
28
34
|
isCollapsed?: boolean
|
|
29
35
|
onToggleCollapse?: () => void
|
|
36
|
+
onTerminalClick?: () => void
|
|
37
|
+
sessionCount?: number
|
|
30
38
|
}) {
|
|
31
39
|
const pathname = usePathname()
|
|
32
40
|
|
|
@@ -94,6 +102,45 @@ function SidebarContent({
|
|
|
94
102
|
}
|
|
95
103
|
return linkContent
|
|
96
104
|
})}
|
|
105
|
+
|
|
106
|
+
{/* Terminal Button */}
|
|
107
|
+
{onTerminalClick && (
|
|
108
|
+
(() => {
|
|
109
|
+
const terminalButton = (
|
|
110
|
+
<button
|
|
111
|
+
onClick={onTerminalClick}
|
|
112
|
+
className={cn(
|
|
113
|
+
'flex items-center rounded-md transition-colors min-h-[44px] w-full',
|
|
114
|
+
isCollapsed ? 'justify-center px-2' : 'gap-3 px-3',
|
|
115
|
+
'py-2.5',
|
|
116
|
+
'text-muted-foreground hover:bg-accent hover:text-accent-foreground'
|
|
117
|
+
)}
|
|
118
|
+
>
|
|
119
|
+
<div className="relative">
|
|
120
|
+
<Terminal className="h-5 w-5 shrink-0" />
|
|
121
|
+
{sessionCount !== undefined && sessionCount > 0 && (
|
|
122
|
+
<Badge
|
|
123
|
+
className="absolute -top-2 -right-2 h-4 w-4 p-0 flex items-center justify-center text-[10px] bg-primary"
|
|
124
|
+
>
|
|
125
|
+
{sessionCount}
|
|
126
|
+
</Badge>
|
|
127
|
+
)}
|
|
128
|
+
</div>
|
|
129
|
+
{!isCollapsed && <span className="text-sm font-medium">Terminal</span>}
|
|
130
|
+
</button>
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
if (isCollapsed) {
|
|
134
|
+
return (
|
|
135
|
+
<Tooltip key="terminal">
|
|
136
|
+
<TooltipTrigger asChild>{terminalButton}</TooltipTrigger>
|
|
137
|
+
<TooltipContent side="right">Terminal</TooltipContent>
|
|
138
|
+
</Tooltip>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
return terminalButton
|
|
142
|
+
})()
|
|
143
|
+
)}
|
|
97
144
|
</div>
|
|
98
145
|
</nav>
|
|
99
146
|
|
|
@@ -138,10 +185,66 @@ function SidebarContent({
|
|
|
138
185
|
)
|
|
139
186
|
}
|
|
140
187
|
|
|
188
|
+
const SIDEBAR_COLLAPSED_KEY = 'prjct-sidebar-collapsed'
|
|
189
|
+
|
|
141
190
|
export function AppSidebar() {
|
|
142
191
|
const [isOpen, setIsOpen] = useState(false)
|
|
143
192
|
const [isMobile, setIsMobile] = useState(false)
|
|
144
193
|
const [isCollapsed, setIsCollapsed] = useState(false)
|
|
194
|
+
const [showProjectSelector, setShowProjectSelector] = useState(false)
|
|
195
|
+
const pathname = usePathname()
|
|
196
|
+
|
|
197
|
+
// Terminal context
|
|
198
|
+
const {
|
|
199
|
+
toggleDock,
|
|
200
|
+
openDock,
|
|
201
|
+
getTotalSessionCount,
|
|
202
|
+
projectSessions,
|
|
203
|
+
createSessionForProject,
|
|
204
|
+
} = useGlobalTerminal()
|
|
205
|
+
|
|
206
|
+
const sessionCount = getTotalSessionCount()
|
|
207
|
+
|
|
208
|
+
// Handle terminal button click
|
|
209
|
+
const handleTerminalClick = () => {
|
|
210
|
+
// If we're on a project page, auto-detect project
|
|
211
|
+
const projectMatch = pathname?.match(/\/project\/([^/]+)/)
|
|
212
|
+
|
|
213
|
+
if (projectMatch) {
|
|
214
|
+
const projectId = projectMatch[1]
|
|
215
|
+
const existingProject = projectSessions.get(projectId)
|
|
216
|
+
|
|
217
|
+
if (existingProject) {
|
|
218
|
+
// Project already has sessions, just toggle dock
|
|
219
|
+
toggleDock()
|
|
220
|
+
} else {
|
|
221
|
+
// Need to show project selector to get project details
|
|
222
|
+
// For now, open dock (will be empty) - the user should create session from the dock
|
|
223
|
+
setShowProjectSelector(true)
|
|
224
|
+
}
|
|
225
|
+
} else if (sessionCount > 0) {
|
|
226
|
+
// Has sessions, toggle dock
|
|
227
|
+
toggleDock()
|
|
228
|
+
} else {
|
|
229
|
+
// No sessions and no project context - show project selector
|
|
230
|
+
setShowProjectSelector(true)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Load collapsed state from localStorage on mount
|
|
235
|
+
useEffect(() => {
|
|
236
|
+
const stored = localStorage.getItem(SIDEBAR_COLLAPSED_KEY)
|
|
237
|
+
if (stored !== null) {
|
|
238
|
+
setIsCollapsed(stored === 'true')
|
|
239
|
+
}
|
|
240
|
+
}, [])
|
|
241
|
+
|
|
242
|
+
// Persist collapsed state to localStorage
|
|
243
|
+
const handleToggleCollapse = () => {
|
|
244
|
+
const newValue = !isCollapsed
|
|
245
|
+
setIsCollapsed(newValue)
|
|
246
|
+
localStorage.setItem(SIDEBAR_COLLAPSED_KEY, String(newValue))
|
|
247
|
+
}
|
|
145
248
|
|
|
146
249
|
// Detect mobile on mount and resize
|
|
147
250
|
useEffect(() => {
|
|
@@ -166,7 +269,11 @@ export function AppSidebar() {
|
|
|
166
269
|
</button>
|
|
167
270
|
</SheetTrigger>
|
|
168
271
|
<SheetContent side="left" className="w-[280px] p-0 flex flex-col">
|
|
169
|
-
<SidebarContent
|
|
272
|
+
<SidebarContent
|
|
273
|
+
onNavigate={() => setIsOpen(false)}
|
|
274
|
+
onTerminalClick={handleTerminalClick}
|
|
275
|
+
sessionCount={sessionCount}
|
|
276
|
+
/>
|
|
170
277
|
</SheetContent>
|
|
171
278
|
</Sheet>
|
|
172
279
|
)
|
|
@@ -174,15 +281,32 @@ export function AppSidebar() {
|
|
|
174
281
|
|
|
175
282
|
// Desktop: Collapsible sidebar
|
|
176
283
|
return (
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
284
|
+
<>
|
|
285
|
+
<aside className={cn(
|
|
286
|
+
"hidden md:flex h-full flex-col border-r border-border bg-card transition-all duration-200",
|
|
287
|
+
isCollapsed ? "w-14" : "w-60"
|
|
288
|
+
)}>
|
|
289
|
+
<SidebarContent
|
|
290
|
+
isCollapsed={isCollapsed}
|
|
291
|
+
onToggleCollapse={handleToggleCollapse}
|
|
292
|
+
onTerminalClick={handleTerminalClick}
|
|
293
|
+
sessionCount={sessionCount}
|
|
294
|
+
/>
|
|
295
|
+
</aside>
|
|
296
|
+
|
|
297
|
+
{/* Project Selector Modal - will be implemented separately */}
|
|
298
|
+
{showProjectSelector && (
|
|
299
|
+
<ProjectSelectorModal
|
|
300
|
+
isOpen={showProjectSelector}
|
|
301
|
+
onClose={() => setShowProjectSelector(false)}
|
|
302
|
+
onSelectProject={(projectId, projectName, projectPath) => {
|
|
303
|
+
createSessionForProject(projectId, projectName, projectPath)
|
|
304
|
+
openDock()
|
|
305
|
+
setShowProjectSelector(false)
|
|
306
|
+
}}
|
|
307
|
+
/>
|
|
308
|
+
)}
|
|
309
|
+
</>
|
|
186
310
|
)
|
|
187
311
|
}
|
|
188
312
|
|
|
@@ -10,7 +10,7 @@ export const BENTO_SIZE_CLASSES: Record<BentoSize, string> = {
|
|
|
10
10
|
|
|
11
11
|
export const ACCENT_STYLES: Record<AccentColor, string> = {
|
|
12
12
|
default: '',
|
|
13
|
-
success: 'border-
|
|
14
|
-
warning: 'border-
|
|
15
|
-
destructive: 'border-
|
|
13
|
+
success: 'border-muted-foreground/20',
|
|
14
|
+
warning: 'border-muted-foreground/20',
|
|
15
|
+
destructive: 'border-muted-foreground/20',
|
|
16
16
|
}
|
|
@@ -19,6 +19,7 @@ export function BentoCard({
|
|
|
19
19
|
'p-3 sm:p-4',
|
|
20
20
|
'hover:shadow-md hover:border-foreground/20',
|
|
21
21
|
'active:scale-[0.99]',
|
|
22
|
+
'min-w-0 max-w-full',
|
|
22
23
|
BENTO_SIZE_CLASSES[size],
|
|
23
24
|
ACCENT_STYLES[accentColor],
|
|
24
25
|
className
|
|
@@ -29,7 +30,7 @@ export function BentoCard({
|
|
|
29
30
|
<div className="flex items-center gap-2">
|
|
30
31
|
{Icon && <Icon className="h-4 w-4 sm:h-3.5 sm:w-3.5 text-muted-foreground" />}
|
|
31
32
|
{title && (
|
|
32
|
-
<span className="text-
|
|
33
|
+
<span className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
|
|
33
34
|
{title}
|
|
34
35
|
</span>
|
|
35
36
|
)}
|
|
@@ -7,8 +7,8 @@ export function BentoGrid({ className, children }: BentoGridProps) {
|
|
|
7
7
|
className={cn(
|
|
8
8
|
'grid',
|
|
9
9
|
'gap-3 sm:gap-4',
|
|
10
|
-
'grid-cols-1 sm:grid-cols-2 lg:grid-cols-
|
|
11
|
-
'auto-rows-[minmax(
|
|
10
|
+
'grid-cols-1 sm:grid-cols-2 lg:grid-cols-3',
|
|
11
|
+
'auto-rows-[minmax(160px,auto)] sm:auto-rows-[minmax(180px,auto)]',
|
|
12
12
|
className
|
|
13
13
|
)}
|
|
14
14
|
>
|
|
@@ -1,67 +1,75 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { AlertTriangle, Clock, CheckCircle2 } from 'lucide-react'
|
|
5
|
+
import { ExpandButton } from '@/components/ExpandButton'
|
|
3
6
|
import { cn } from '@/lib/utils'
|
|
4
7
|
import type { BlockersCardProps } from './BlockersCard.types'
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
const COLLAPSED_LIMIT = 5
|
|
10
|
+
|
|
11
|
+
export function BlockersCard({ blockers, codeHref, className }: BlockersCardProps) {
|
|
12
|
+
const [expanded, setExpanded] = useState(false)
|
|
7
13
|
const hasBlockers = blockers.length > 0
|
|
14
|
+
const displayBlockers = expanded ? blockers : blockers.slice(0, COLLAPSED_LIMIT)
|
|
15
|
+
const hasMore = blockers.length > COLLAPSED_LIMIT
|
|
8
16
|
|
|
9
17
|
return (
|
|
10
|
-
<
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
className=
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
</div>
|
|
26
|
-
) : (
|
|
27
|
-
<>
|
|
28
|
-
<div className="flex items-center gap-2 mb-2">
|
|
29
|
-
<span className="text-2xl font-bold text-amber-600 dark:text-amber-400">
|
|
30
|
-
{blockers.length}
|
|
31
|
-
</span>
|
|
32
|
-
<span className="text-xs text-muted-foreground">
|
|
33
|
-
{blockers.length === 1 ? 'task blocked' : 'tasks blocked'}
|
|
34
|
-
</span>
|
|
35
|
-
</div>
|
|
36
|
-
|
|
37
|
-
<div className="flex-1 overflow-auto space-y-2">
|
|
38
|
-
{blockers.slice(0, 3).map((blocker, i) => (
|
|
39
|
-
<div
|
|
40
|
-
key={i}
|
|
41
|
-
className="p-2 rounded-md bg-amber-500/10 dark:bg-amber-500/5 border border-amber-500/20"
|
|
42
|
-
>
|
|
43
|
-
<p className="text-xs font-medium truncate">{blocker.task}</p>
|
|
44
|
-
<p className="text-xs text-muted-foreground truncate mt-0.5">
|
|
45
|
-
{blocker.reason}
|
|
46
|
-
</p>
|
|
47
|
-
<div className="flex items-center gap-1 mt-1">
|
|
48
|
-
<Clock className="h-3 w-3 text-muted-foreground" />
|
|
49
|
-
<span className="text-xs text-muted-foreground">
|
|
50
|
-
{blocker.daysBlocked}d
|
|
51
|
-
</span>
|
|
52
|
-
</div>
|
|
53
|
-
</div>
|
|
54
|
-
))}
|
|
55
|
-
</div>
|
|
56
|
-
|
|
57
|
-
{blockers.length > 3 && (
|
|
58
|
-
<p className="text-xs text-muted-foreground mt-2">
|
|
59
|
-
+{blockers.length - 3} more
|
|
60
|
-
</p>
|
|
61
|
-
)}
|
|
62
|
-
</>
|
|
18
|
+
<div className={cn(
|
|
19
|
+
'relative overflow-hidden rounded-xl border bg-card p-4',
|
|
20
|
+
className
|
|
21
|
+
)}>
|
|
22
|
+
<div className="flex items-center justify-between mb-3">
|
|
23
|
+
<div className="flex items-center gap-2">
|
|
24
|
+
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
|
|
25
|
+
<span className="text-xs font-bold uppercase tracking-[0.15em] text-muted-foreground">
|
|
26
|
+
Blockers
|
|
27
|
+
</span>
|
|
28
|
+
</div>
|
|
29
|
+
{hasBlockers && (
|
|
30
|
+
<span className="text-xs font-bold text-muted-foreground tabular-nums">
|
|
31
|
+
{blockers.length}
|
|
32
|
+
</span>
|
|
63
33
|
)}
|
|
64
34
|
</div>
|
|
65
|
-
|
|
35
|
+
|
|
36
|
+
{!hasBlockers ? (
|
|
37
|
+
<div className="flex items-center gap-3 py-2">
|
|
38
|
+
<CheckCircle2 className="h-5 w-5 text-muted-foreground" />
|
|
39
|
+
<div>
|
|
40
|
+
<p className="text-sm font-medium">No blockers</p>
|
|
41
|
+
<p className="text-xs text-muted-foreground">Keep the momentum</p>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
) : (
|
|
45
|
+
<div className="space-y-2">
|
|
46
|
+
{displayBlockers.map((blocker, i) => (
|
|
47
|
+
<div
|
|
48
|
+
key={i}
|
|
49
|
+
className="p-2.5 rounded-lg bg-muted/50 border"
|
|
50
|
+
>
|
|
51
|
+
<p className="text-sm font-medium">{blocker.task}</p>
|
|
52
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
53
|
+
{blocker.reason}
|
|
54
|
+
</p>
|
|
55
|
+
<div className="flex items-center gap-1 mt-1.5">
|
|
56
|
+
<Clock className="h-3 w-3 text-muted-foreground" />
|
|
57
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
58
|
+
{blocker.daysBlocked} day{blocker.daysBlocked !== 1 ? 's' : ''} blocked
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
))}
|
|
63
|
+
{hasMore && (
|
|
64
|
+
<ExpandButton
|
|
65
|
+
expanded={expanded}
|
|
66
|
+
totalCount={blockers.length}
|
|
67
|
+
collapsedLimit={COLLAPSED_LIMIT}
|
|
68
|
+
onToggle={() => setExpanded(!expanded)}
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
66
74
|
)
|
|
67
75
|
}
|