feat: add project reset functionality with quick and full reset options

Add the ability to reset a project to its initial state with two options:
- Quick Reset: Clears features.db, assistant.db, and settings files while
  preserving app spec and prompts
- Full Reset: Deletes everything including prompts directory, triggering
  the setup wizard for project reconfiguration

Backend changes:
- Add POST /{name}/reset endpoint to projects router with full_reset query param
- Validate agent lock file to prevent reset while agent is running (409 Conflict)
- Dispose database engines before deleting files to release Windows file locks
- Add engine caching to api/database.py for better connection management
- Add dispose_engine() functions to both database modules
- Delete WAL mode journal files (*.db-wal, *.db-shm) during reset

Frontend changes:
- Add ResetProjectModal component with toggle between Quick/Full reset modes
- Add ProjectSetupRequired component shown when has_spec is false
- Add resetProject API function and useResetProject React Query hook
- Integrate reset button in header (disabled when agent running)
- Add 'R' keyboard shortcut to open reset modal
- Show ProjectSetupRequired when project needs setup after full reset

This implements the feature from PR #4 directly on master to avoid merge
conflicts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Auto
2026-01-29 10:38:48 +02:00
parent 836bc8ae16
commit cf62885e83
8 changed files with 500 additions and 2 deletions

View File

@@ -26,8 +26,10 @@ import { ViewToggle, type ViewMode } from './components/ViewToggle'
import { DependencyGraph } from './components/DependencyGraph'
import { KeyboardShortcutsHelp } from './components/KeyboardShortcutsHelp'
import { ThemeSelector } from './components/ThemeSelector'
import { ResetProjectModal } from './components/ResetProjectModal'
import { ProjectSetupRequired } from './components/ProjectSetupRequired'
import { getDependencyGraph } from './lib/api'
import { Loader2, Settings, Moon, Sun } from 'lucide-react'
import { Loader2, Settings, Moon, Sun, RotateCcw } from 'lucide-react'
import type { Feature } from './lib/types'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
@@ -59,6 +61,7 @@ function App() {
const [showSettings, setShowSettings] = useState(false)
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false)
const [isSpecCreating, setIsSpecCreating] = useState(false)
const [showResetModal, setShowResetModal] = useState(false)
const [showSpecChat, setShowSpecChat] = useState(false) // For "Create Spec" button in empty kanban
const [viewMode, setViewMode] = useState<ViewMode>(() => {
try {
@@ -203,10 +206,18 @@ function App() {
setShowKeyboardHelp(true)
}
// R : Open reset modal (when project selected and agent not running)
if ((e.key === 'r' || e.key === 'R') && selectedProject && wsState.agentStatus !== 'running') {
e.preventDefault()
setShowResetModal(true)
}
// Escape : Close modals
if (e.key === 'Escape') {
if (showKeyboardHelp) {
setShowKeyboardHelp(false)
} else if (showResetModal) {
setShowResetModal(false)
} else if (showExpandProject) {
setShowExpandProject(false)
} else if (showSettings) {
@@ -225,7 +236,7 @@ function App() {
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [selectedProject, showAddFeature, showExpandProject, selectedFeature, debugOpen, debugActiveTab, assistantOpen, features, showSettings, showKeyboardHelp, isSpecCreating, viewMode])
}, [selectedProject, showAddFeature, showExpandProject, selectedFeature, debugOpen, debugActiveTab, assistantOpen, features, showSettings, showKeyboardHelp, isSpecCreating, viewMode, showResetModal, wsState.agentStatus])
// Combine WebSocket progress with feature data
const progress = wsState.progress.total > 0 ? wsState.progress : {
@@ -287,6 +298,17 @@ function App() {
<Settings size={18} />
</Button>
<Button
onClick={() => setShowResetModal(true)}
variant="outline"
size="sm"
title="Reset Project (R)"
aria-label="Reset Project"
disabled={wsState.agentStatus === 'running'}
>
<RotateCcw size={18} />
</Button>
{/* Ollama Mode Indicator */}
{settings?.ollama_mode && (
<div
@@ -346,6 +368,16 @@ function App() {
Select a project from the dropdown above or create a new one to get started.
</p>
</div>
) : !hasSpec ? (
<ProjectSetupRequired
projectName={selectedProject}
projectPath={selectedProjectData?.path}
onCreateWithClaude={() => setShowSpecChat(true)}
onEditManually={() => {
// Open debug panel for the user to see the project path
setDebugOpen(true)
}}
/>
) : (
<div className="space-y-8">
{/* Progress Dashboard */}
@@ -512,6 +544,21 @@ function App() {
{/* Keyboard Shortcuts Help */}
<KeyboardShortcutsHelp isOpen={showKeyboardHelp} onClose={() => setShowKeyboardHelp(false)} />
{/* Reset Project Modal */}
{showResetModal && selectedProject && (
<ResetProjectModal
isOpen={showResetModal}
projectName={selectedProject}
onClose={() => setShowResetModal(false)}
onResetComplete={(wasFullReset) => {
// If full reset, the spec was deleted - show spec creation chat
if (wasFullReset) {
setShowSpecChat(true)
}
}}
/>
)}
{/* Celebration Overlay - shows when a feature is completed by an agent */}
{wsState.celebration && (
<CelebrationOverlay

View File

@@ -0,0 +1,90 @@
import { Sparkles, FileEdit, FolderOpen } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
interface ProjectSetupRequiredProps {
projectName: string
projectPath?: string
onCreateWithClaude: () => void
onEditManually: () => void
}
export function ProjectSetupRequired({
projectName,
projectPath,
onCreateWithClaude,
onEditManually,
}: ProjectSetupRequiredProps) {
return (
<div className="max-w-2xl mx-auto mt-8">
<Card className="border-2">
<CardHeader className="text-center">
<CardTitle className="text-2xl font-display">
Project Setup Required
</CardTitle>
<CardDescription className="text-base">
<span className="font-semibold">{projectName}</span> needs an app spec to get started
</CardDescription>
{projectPath && (
<div className="flex items-center justify-center gap-2 text-sm text-muted-foreground mt-2">
<FolderOpen size={14} />
<code className="bg-muted px-2 py-0.5 rounded text-xs">{projectPath}</code>
</div>
)}
</CardHeader>
<CardContent className="space-y-4">
<p className="text-center text-muted-foreground">
Choose how you want to create your app specification:
</p>
<div className="grid gap-4 md:grid-cols-2">
{/* Create with Claude Option */}
<Card
className="cursor-pointer border-2 transition-all hover:border-primary hover:shadow-md"
onClick={onCreateWithClaude}
>
<CardContent className="pt-6 text-center space-y-3">
<div className="w-12 h-12 mx-auto bg-primary/10 rounded-full flex items-center justify-center">
<Sparkles className="text-primary" size={24} />
</div>
<h3 className="font-semibold text-lg">Create with Claude</h3>
<p className="text-sm text-muted-foreground">
Describe your app idea and Claude will help create a detailed specification
</p>
<Button className="w-full">
<Sparkles size={16} className="mr-2" />
Start Chat
</Button>
</CardContent>
</Card>
{/* Edit Manually Option */}
<Card
className="cursor-pointer border-2 transition-all hover:border-primary hover:shadow-md"
onClick={onEditManually}
>
<CardContent className="pt-6 text-center space-y-3">
<div className="w-12 h-12 mx-auto bg-muted rounded-full flex items-center justify-center">
<FileEdit className="text-muted-foreground" size={24} />
</div>
<h3 className="font-semibold text-lg">Edit Templates Manually</h3>
<p className="text-sm text-muted-foreground">
Create the prompts directory and edit template files yourself
</p>
<Button variant="outline" className="w-full">
<FileEdit size={16} className="mr-2" />
View Templates
</Button>
</CardContent>
</Card>
</div>
<p className="text-center text-xs text-muted-foreground pt-4">
The app spec tells the agent what to build. It includes the application name,
description, tech stack, and feature requirements.
</p>
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,194 @@
import { useState } from 'react'
import { Loader2, AlertTriangle, RotateCcw, Trash2, Check, X } from 'lucide-react'
import { useResetProject } from '../hooks/useProjects'
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Alert, AlertDescription } from '@/components/ui/alert'
interface ResetProjectModalProps {
isOpen: boolean
projectName: string
onClose: () => void
onResetComplete?: (wasFullReset: boolean) => void
}
export function ResetProjectModal({
isOpen,
projectName,
onClose,
onResetComplete,
}: ResetProjectModalProps) {
const [resetType, setResetType] = useState<'quick' | 'full'>('quick')
const resetProject = useResetProject(projectName)
const handleReset = async () => {
const isFullReset = resetType === 'full'
try {
await resetProject.mutateAsync(isFullReset)
onResetComplete?.(isFullReset)
onClose()
} catch {
// Error is handled by the mutation state
}
}
const handleClose = () => {
if (!resetProject.isPending) {
resetProject.reset()
setResetType('quick')
onClose()
}
}
return (
<Dialog open={isOpen} onOpenChange={(open) => !open && handleClose()}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<RotateCcw size={20} />
Reset Project
</DialogTitle>
<DialogDescription>
Reset <span className="font-semibold">{projectName}</span> to start fresh
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-4">
{/* Reset Type Toggle */}
<div className="flex rounded-lg border-2 border-border overflow-hidden">
<button
onClick={() => setResetType('quick')}
disabled={resetProject.isPending}
className={`flex-1 py-3 px-4 text-sm font-medium transition-colors flex items-center justify-center gap-2 ${
resetType === 'quick'
? 'bg-primary text-primary-foreground'
: 'bg-background text-foreground hover:bg-muted'
} ${resetProject.isPending ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<RotateCcw size={16} />
Quick Reset
</button>
<button
onClick={() => setResetType('full')}
disabled={resetProject.isPending}
className={`flex-1 py-3 px-4 text-sm font-medium transition-colors flex items-center justify-center gap-2 ${
resetType === 'full'
? 'bg-destructive text-destructive-foreground'
: 'bg-background text-foreground hover:bg-muted'
} ${resetProject.isPending ? 'opacity-50 cursor-not-allowed' : ''}`}
>
<Trash2 size={16} />
Full Reset
</button>
</div>
{/* Warning Box */}
<Alert variant={resetType === 'full' ? 'destructive' : 'default'} className="border-2">
<AlertTriangle className="h-4 w-4" />
<AlertDescription>
<div className="font-semibold mb-2">
{resetType === 'quick' ? 'What will be deleted:' : 'What will be deleted:'}
</div>
<ul className="list-none space-y-1 text-sm">
<li className="flex items-center gap-2">
<X size={14} className="text-destructive" />
All features and progress
</li>
<li className="flex items-center gap-2">
<X size={14} className="text-destructive" />
Assistant chat history
</li>
<li className="flex items-center gap-2">
<X size={14} className="text-destructive" />
Agent settings
</li>
{resetType === 'full' && (
<li className="flex items-center gap-2">
<X size={14} className="text-destructive" />
App spec and prompts
</li>
)}
</ul>
</AlertDescription>
</Alert>
{/* What will be preserved */}
<div className="bg-muted/50 rounded-lg border-2 border-border p-3">
<div className="font-semibold mb-2 text-sm">
{resetType === 'quick' ? 'What will be preserved:' : 'What will be preserved:'}
</div>
<ul className="list-none space-y-1 text-sm text-muted-foreground">
{resetType === 'quick' ? (
<>
<li className="flex items-center gap-2">
<Check size={14} className="text-green-600" />
App spec and prompts
</li>
<li className="flex items-center gap-2">
<Check size={14} className="text-green-600" />
Project code and files
</li>
</>
) : (
<>
<li className="flex items-center gap-2">
<Check size={14} className="text-green-600" />
Project code and files
</li>
<li className="flex items-center gap-2 text-muted-foreground/70">
<AlertTriangle size={14} />
Setup wizard will appear
</li>
</>
)}
</ul>
</div>
{/* Error Message */}
{resetProject.isError && (
<Alert variant="destructive">
<AlertDescription>
{resetProject.error instanceof Error
? resetProject.error.message
: 'Failed to reset project. Please try again.'}
</AlertDescription>
</Alert>
)}
</div>
<DialogFooter className="gap-2">
<Button
variant="outline"
onClick={handleClose}
disabled={resetProject.isPending}
>
Cancel
</Button>
<Button
variant={resetType === 'full' ? 'destructive' : 'default'}
onClick={handleReset}
disabled={resetProject.isPending}
>
{resetProject.isPending ? (
<>
<Loader2 className="animate-spin mr-2" size={16} />
Resetting...
</>
) : (
<>
{resetType === 'quick' ? 'Quick Reset' : 'Full Reset'}
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}

View File

@@ -48,6 +48,20 @@ export function useDeleteProject() {
})
}
export function useResetProject(projectName: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: (fullReset: boolean) => api.resetProject(projectName, fullReset),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['projects'] })
queryClient.invalidateQueries({ queryKey: ['project', projectName] })
queryClient.invalidateQueries({ queryKey: ['features', projectName] })
queryClient.invalidateQueries({ queryKey: ['agent-status', projectName] })
},
})
}
export function useUpdateProjectSettings(projectName: string) {
const queryClient = useQueryClient()

View File

@@ -111,6 +111,23 @@ export async function updateProjectSettings(
})
}
export interface ResetProjectResponse {
success: boolean
reset_type: 'quick' | 'full'
deleted_files: string[]
message: string
}
export async function resetProject(
name: string,
fullReset: boolean = false
): Promise<ResetProjectResponse> {
const params = fullReset ? '?full_reset=true' : ''
return fetchJSON(`/projects/${encodeURIComponent(name)}/reset${params}`, {
method: 'POST',
})
}
// ============================================================================
// Features API
// ============================================================================