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

@@ -373,6 +373,87 @@ async def get_project_stats_endpoint(name: str):
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)
async def update_project_settings(name: str, settings: ProjectSettingsUpdate):
"""Update project-level settings (concurrency, etc.)."""

View File

@@ -79,6 +79,26 @@ def get_engine(project_dir: Path):
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):
"""Get a new database session for a project."""
engine = get_engine(project_dir)