mirror of
https://github.com/leonvanzyl/autocoder.git
synced 2026-01-29 22:02:05 +00:00
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:
@@ -336,12 +336,20 @@ def create_database(project_dir: Path) -> tuple:
|
|||||||
"""
|
"""
|
||||||
Create database and return engine + session maker.
|
Create database and return engine + session maker.
|
||||||
|
|
||||||
|
Uses a cache to avoid creating new engines for each request, which improves
|
||||||
|
performance by reusing database connections.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
project_dir: Directory containing the project
|
project_dir: Directory containing the project
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (engine, SessionLocal)
|
Tuple of (engine, SessionLocal)
|
||||||
"""
|
"""
|
||||||
|
cache_key = project_dir.as_posix()
|
||||||
|
|
||||||
|
if cache_key in _engine_cache:
|
||||||
|
return _engine_cache[cache_key]
|
||||||
|
|
||||||
db_url = get_database_url(project_dir)
|
db_url = get_database_url(project_dir)
|
||||||
engine = create_engine(db_url, connect_args={
|
engine = create_engine(db_url, connect_args={
|
||||||
"check_same_thread": False,
|
"check_same_thread": False,
|
||||||
@@ -369,12 +377,39 @@ def create_database(project_dir: Path) -> tuple:
|
|||||||
_migrate_add_schedules_tables(engine)
|
_migrate_add_schedules_tables(engine)
|
||||||
|
|
||||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||||
|
|
||||||
|
# Cache the engine and session maker
|
||||||
|
_engine_cache[cache_key] = (engine, SessionLocal)
|
||||||
|
|
||||||
return engine, SessionLocal
|
return engine, SessionLocal
|
||||||
|
|
||||||
|
|
||||||
|
def dispose_engine(project_dir: Path) -> bool:
|
||||||
|
"""Dispose of and remove the cached engine for a project.
|
||||||
|
|
||||||
|
This closes all database connections, releasing file locks on Windows.
|
||||||
|
Should be called before deleting the database file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if an engine was disposed, False if no engine was cached.
|
||||||
|
"""
|
||||||
|
cache_key = project_dir.as_posix()
|
||||||
|
|
||||||
|
if cache_key in _engine_cache:
|
||||||
|
engine, _ = _engine_cache.pop(cache_key)
|
||||||
|
engine.dispose()
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# Global session maker - will be set when server starts
|
# Global session maker - will be set when server starts
|
||||||
_session_maker: Optional[sessionmaker] = None
|
_session_maker: Optional[sessionmaker] = None
|
||||||
|
|
||||||
|
# Engine cache to avoid creating new engines for each request
|
||||||
|
# Key: project directory path (as posix string), Value: (engine, SessionLocal)
|
||||||
|
_engine_cache: dict[str, tuple] = {}
|
||||||
|
|
||||||
|
|
||||||
def set_session_maker(session_maker: sessionmaker) -> None:
|
def set_session_maker(session_maker: sessionmaker) -> None:
|
||||||
"""Set the global session maker."""
|
"""Set the global session maker."""
|
||||||
|
|||||||
@@ -373,6 +373,87 @@ async def get_project_stats_endpoint(name: str):
|
|||||||
return get_project_stats(project_dir)
|
return get_project_stats(project_dir)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/{name}/reset")
|
||||||
|
async def reset_project(name: str, full_reset: bool = False):
|
||||||
|
"""
|
||||||
|
Reset a project to its initial state.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: Project name to reset
|
||||||
|
full_reset: If True, also delete prompts/ directory (triggers setup wizard)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with list of deleted files and reset type
|
||||||
|
"""
|
||||||
|
_init_imports()
|
||||||
|
(_, _, get_project_path, _, _, _, _) = _get_registry_functions()
|
||||||
|
|
||||||
|
name = validate_project_name(name)
|
||||||
|
project_dir = get_project_path(name)
|
||||||
|
|
||||||
|
if not project_dir:
|
||||||
|
raise HTTPException(status_code=404, detail=f"Project '{name}' not found")
|
||||||
|
|
||||||
|
if not project_dir.exists():
|
||||||
|
raise HTTPException(status_code=404, detail="Project directory not found")
|
||||||
|
|
||||||
|
# Check if agent is running
|
||||||
|
lock_file = project_dir / ".agent.lock"
|
||||||
|
if lock_file.exists():
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=409,
|
||||||
|
detail="Cannot reset project while agent is running. Stop the agent first."
|
||||||
|
)
|
||||||
|
|
||||||
|
# Dispose of database engines to release file locks (required on Windows)
|
||||||
|
# Import here to avoid circular imports
|
||||||
|
from api.database import dispose_engine as dispose_features_engine
|
||||||
|
from server.services.assistant_database import dispose_engine as dispose_assistant_engine
|
||||||
|
|
||||||
|
dispose_features_engine(project_dir)
|
||||||
|
dispose_assistant_engine(project_dir)
|
||||||
|
|
||||||
|
deleted_files: list[str] = []
|
||||||
|
|
||||||
|
# Files to delete in quick reset
|
||||||
|
quick_reset_files = [
|
||||||
|
"features.db",
|
||||||
|
"features.db-wal", # WAL mode journal file
|
||||||
|
"features.db-shm", # WAL mode shared memory file
|
||||||
|
"assistant.db",
|
||||||
|
"assistant.db-wal",
|
||||||
|
"assistant.db-shm",
|
||||||
|
".claude_settings.json",
|
||||||
|
".claude_assistant_settings.json",
|
||||||
|
]
|
||||||
|
|
||||||
|
for filename in quick_reset_files:
|
||||||
|
file_path = project_dir / filename
|
||||||
|
if file_path.exists():
|
||||||
|
try:
|
||||||
|
file_path.unlink()
|
||||||
|
deleted_files.append(filename)
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to delete {filename}: {e}")
|
||||||
|
|
||||||
|
# Full reset: also delete prompts directory
|
||||||
|
if full_reset:
|
||||||
|
prompts_dir = project_dir / "prompts"
|
||||||
|
if prompts_dir.exists():
|
||||||
|
try:
|
||||||
|
shutil.rmtree(prompts_dir)
|
||||||
|
deleted_files.append("prompts/")
|
||||||
|
except Exception as e:
|
||||||
|
raise HTTPException(status_code=500, detail=f"Failed to delete prompts/: {e}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"reset_type": "full" if full_reset else "quick",
|
||||||
|
"deleted_files": deleted_files,
|
||||||
|
"message": f"Project '{name}' has been reset" + (" (full reset)" if full_reset else " (quick reset)")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.patch("/{name}/settings", response_model=ProjectDetail)
|
@router.patch("/{name}/settings", response_model=ProjectDetail)
|
||||||
async def update_project_settings(name: str, settings: ProjectSettingsUpdate):
|
async def update_project_settings(name: str, settings: ProjectSettingsUpdate):
|
||||||
"""Update project-level settings (concurrency, etc.)."""
|
"""Update project-level settings (concurrency, etc.)."""
|
||||||
|
|||||||
@@ -79,6 +79,26 @@ def get_engine(project_dir: Path):
|
|||||||
return _engine_cache[cache_key]
|
return _engine_cache[cache_key]
|
||||||
|
|
||||||
|
|
||||||
|
def dispose_engine(project_dir: Path) -> bool:
|
||||||
|
"""Dispose of and remove the cached engine for a project.
|
||||||
|
|
||||||
|
This closes all database connections, releasing file locks on Windows.
|
||||||
|
Should be called before deleting the database file.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if an engine was disposed, False if no engine was cached.
|
||||||
|
"""
|
||||||
|
cache_key = project_dir.as_posix()
|
||||||
|
|
||||||
|
if cache_key in _engine_cache:
|
||||||
|
engine = _engine_cache.pop(cache_key)
|
||||||
|
engine.dispose()
|
||||||
|
logger.debug(f"Disposed database engine for {cache_key}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_session(project_dir: Path):
|
def get_session(project_dir: Path):
|
||||||
"""Get a new database session for a project."""
|
"""Get a new database session for a project."""
|
||||||
engine = get_engine(project_dir)
|
engine = get_engine(project_dir)
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ import { ViewToggle, type ViewMode } from './components/ViewToggle'
|
|||||||
import { DependencyGraph } from './components/DependencyGraph'
|
import { DependencyGraph } from './components/DependencyGraph'
|
||||||
import { KeyboardShortcutsHelp } from './components/KeyboardShortcutsHelp'
|
import { KeyboardShortcutsHelp } from './components/KeyboardShortcutsHelp'
|
||||||
import { ThemeSelector } from './components/ThemeSelector'
|
import { ThemeSelector } from './components/ThemeSelector'
|
||||||
|
import { ResetProjectModal } from './components/ResetProjectModal'
|
||||||
|
import { ProjectSetupRequired } from './components/ProjectSetupRequired'
|
||||||
import { getDependencyGraph } from './lib/api'
|
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 type { Feature } from './lib/types'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Card, CardContent } from '@/components/ui/card'
|
import { Card, CardContent } from '@/components/ui/card'
|
||||||
@@ -59,6 +61,7 @@ function App() {
|
|||||||
const [showSettings, setShowSettings] = useState(false)
|
const [showSettings, setShowSettings] = useState(false)
|
||||||
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false)
|
const [showKeyboardHelp, setShowKeyboardHelp] = useState(false)
|
||||||
const [isSpecCreating, setIsSpecCreating] = 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 [showSpecChat, setShowSpecChat] = useState(false) // For "Create Spec" button in empty kanban
|
||||||
const [viewMode, setViewMode] = useState<ViewMode>(() => {
|
const [viewMode, setViewMode] = useState<ViewMode>(() => {
|
||||||
try {
|
try {
|
||||||
@@ -203,10 +206,18 @@ function App() {
|
|||||||
setShowKeyboardHelp(true)
|
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
|
// Escape : Close modals
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
if (showKeyboardHelp) {
|
if (showKeyboardHelp) {
|
||||||
setShowKeyboardHelp(false)
|
setShowKeyboardHelp(false)
|
||||||
|
} else if (showResetModal) {
|
||||||
|
setShowResetModal(false)
|
||||||
} else if (showExpandProject) {
|
} else if (showExpandProject) {
|
||||||
setShowExpandProject(false)
|
setShowExpandProject(false)
|
||||||
} else if (showSettings) {
|
} else if (showSettings) {
|
||||||
@@ -225,7 +236,7 @@ function App() {
|
|||||||
|
|
||||||
window.addEventListener('keydown', handleKeyDown)
|
window.addEventListener('keydown', handleKeyDown)
|
||||||
return () => window.removeEventListener('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
|
// Combine WebSocket progress with feature data
|
||||||
const progress = wsState.progress.total > 0 ? wsState.progress : {
|
const progress = wsState.progress.total > 0 ? wsState.progress : {
|
||||||
@@ -287,6 +298,17 @@ function App() {
|
|||||||
<Settings size={18} />
|
<Settings size={18} />
|
||||||
</Button>
|
</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 */}
|
{/* Ollama Mode Indicator */}
|
||||||
{settings?.ollama_mode && (
|
{settings?.ollama_mode && (
|
||||||
<div
|
<div
|
||||||
@@ -346,6 +368,16 @@ function App() {
|
|||||||
Select a project from the dropdown above or create a new one to get started.
|
Select a project from the dropdown above or create a new one to get started.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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">
|
<div className="space-y-8">
|
||||||
{/* Progress Dashboard */}
|
{/* Progress Dashboard */}
|
||||||
@@ -512,6 +544,21 @@ function App() {
|
|||||||
{/* Keyboard Shortcuts Help */}
|
{/* Keyboard Shortcuts Help */}
|
||||||
<KeyboardShortcutsHelp isOpen={showKeyboardHelp} onClose={() => setShowKeyboardHelp(false)} />
|
<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 */}
|
{/* Celebration Overlay - shows when a feature is completed by an agent */}
|
||||||
{wsState.celebration && (
|
{wsState.celebration && (
|
||||||
<CelebrationOverlay
|
<CelebrationOverlay
|
||||||
|
|||||||
90
ui/src/components/ProjectSetupRequired.tsx
Normal file
90
ui/src/components/ProjectSetupRequired.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
194
ui/src/components/ResetProjectModal.tsx
Normal file
194
ui/src/components/ResetProjectModal.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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) {
|
export function useUpdateProjectSettings(projectName: string) {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
// Features API
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
Reference in New Issue
Block a user