diff --git a/server/routers/projects.py b/server/routers/projects.py index 2e190fba..b7f01fc5 100644 --- a/server/routers/projects.py +++ b/server/routers/projects.py @@ -259,6 +259,100 @@ async def delete_project(name: str, delete_files: bool = False): } +class ResetOptions: + """Options for project reset.""" + delete_prompts: bool = False + + +@router.post("/{name}/reset") +async def reset_project(name: str, full_reset: bool = False): + """ + Reset a project to its initial state. + + This clears all features, assistant chat history, and settings. + Use this to restart a project from scratch without having to re-register it. + + Args: + name: Project name to reset + full_reset: If True, also deletes prompts directory for complete fresh start + + Always Deletes: + - features.db (feature tracking database) + - assistant.db (assistant chat history) + - .claude_settings.json (agent settings) + - .claude_assistant_settings.json (assistant settings) + + When full_reset=True, Also Deletes: + - prompts/ directory (app_spec.txt, initializer_prompt.md, coding_prompt.md) + + Preserves: + - Project registration in registry + """ + _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=f"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." + ) + + # Files to delete + files_to_delete = [ + "features.db", + "assistant.db", + ".claude_settings.json", + ".claude_assistant_settings.json", + ] + + deleted_files = [] + errors = [] + + for filename in files_to_delete: + filepath = project_dir / filename + if filepath.exists(): + try: + filepath.unlink() + deleted_files.append(filename) + except Exception as e: + errors.append(f"{filename}: {e}") + + # If 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: + errors.append(f"prompts/: {e}") + + if errors: + raise HTTPException( + status_code=500, + detail=f"Failed to delete some files: {'; '.join(errors)}" + ) + + reset_type = "fully reset" if full_reset else "reset" + return { + "success": True, + "message": f"Project '{name}' has been {reset_type}", + "deleted_files": deleted_files, + "full_reset": full_reset, + } + + @router.get("/{name}/prompts", response_model=ProjectPrompts) async def get_project_prompts(name: str): """Get the content of project prompt files.""" diff --git a/start_ui.sh b/start_ui.sh old mode 100644 new mode 100755 index 317d7448..d7a2ea79 --- a/start_ui.sh +++ b/start_ui.sh @@ -11,6 +11,12 @@ echo "" # Get the directory where this script is located SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# Activate virtual environment if it exists +if [ -d "$SCRIPT_DIR/venv" ]; then + echo "Activating virtual environment..." + source "$SCRIPT_DIR/venv/bin/activate" +fi + # Check if Python is available if ! command -v python3 &> /dev/null; then if ! command -v python &> /dev/null; then diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 96066b24..5cc5cd06 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -16,7 +16,9 @@ import { DebugLogViewer } from './components/DebugLogViewer' import { AgentThought } from './components/AgentThought' import { AssistantFAB } from './components/AssistantFAB' import { AssistantPanel } from './components/AssistantPanel' -import { Plus, Loader2 } from 'lucide-react' +import { ResetProjectModal } from './components/ResetProjectModal' +import { ProjectSetupRequired } from './components/ProjectSetupRequired' +import { Plus, Loader2, RotateCcw } from 'lucide-react' import type { Feature } from './lib/types' function App() { @@ -34,12 +36,19 @@ function App() { const [debugOpen, setDebugOpen] = useState(false) const [debugPanelHeight, setDebugPanelHeight] = useState(288) // Default height const [assistantOpen, setAssistantOpen] = useState(false) + const [showResetModal, setShowResetModal] = useState(false) - const { data: projects, isLoading: projectsLoading } = useProjects() + const { data: projects, isLoading: projectsLoading, refetch: refetchProjects } = useProjects() const { data: features } = useFeatures(selectedProject) const { data: agentStatusData } = useAgentStatus(selectedProject) const wsState = useProjectWebSocket(selectedProject) + // Get the selected project's has_spec status + const selectedProjectData = selectedProject + ? projects?.find(p => p.name === selectedProject) + : null + const needsSetup = selectedProjectData?.has_spec === false + // Play sounds when features move between columns useFeatureSound(features) @@ -95,7 +104,9 @@ function App() { // Escape : Close modals if (e.key === 'Escape') { - if (assistantOpen) { + if (showResetModal) { + setShowResetModal(false) + } else if (assistantOpen) { setAssistantOpen(false) } else if (showAddFeature) { setShowAddFeature(false) @@ -109,7 +120,7 @@ function App() { window.addEventListener('keydown', handleKeyDown) return () => window.removeEventListener('keydown', handleKeyDown) - }, [selectedProject, showAddFeature, selectedFeature, debugOpen, assistantOpen]) + }, [selectedProject, showAddFeature, selectedFeature, debugOpen, assistantOpen, showResetModal, wsState.agentStatus]) // Combine WebSocket progress with feature data const progress = wsState.progress.total > 0 ? wsState.progress : { @@ -160,6 +171,16 @@ function App() { + + + ) : needsSetup ? ( + { + // Refetch projects to update has_spec status + refetchProjects() + }} + /> ) : (
{/* Progress Dashboard */} @@ -245,6 +274,14 @@ function App() { /> )} + {/* Reset Project Modal */} + {showResetModal && selectedProject && ( + setShowResetModal(false)} + /> + )} + {/* Debug Log Viewer - fixed to bottom */} {selectedProject && ( void +} + +export function ProjectSetupRequired({ projectName, onSetupComplete }: ProjectSetupRequiredProps) { + const [showChat, setShowChat] = useState(false) + const [initializerStatus, setInitializerStatus] = useState('idle') + const [initializerError, setInitializerError] = useState(null) + const [yoloModeSelected, setYoloModeSelected] = useState(false) + + const handleClaudeSelect = () => { + setShowChat(true) + } + + const handleManualSelect = () => { + // For manual, just refresh to show the empty project + // User can edit prompts/app_spec.txt directly + onSetupComplete() + } + + const handleSpecComplete = async (_specPath: string, yoloMode: boolean = false) => { + setYoloModeSelected(yoloMode) + setInitializerStatus('starting') + try { + await startAgent(projectName, yoloMode) + onSetupComplete() + } catch (err) { + setInitializerStatus('error') + setInitializerError(err instanceof Error ? err.message : 'Failed to start agent') + } + } + + const handleRetryInitializer = () => { + setInitializerError(null) + setInitializerStatus('idle') + handleSpecComplete('', yoloModeSelected) + } + + const handleChatCancel = () => { + setShowChat(false) + } + + const handleExitToProject = () => { + onSetupComplete() + } + + // Full-screen chat view + if (showChat) { + return ( +
+ +
+ ) + } + + return ( +
+ {/* Header */} +
+
+ +
+
+

Setup Required

+

+ Project {projectName} needs an app specification to get started. +

+
+
+ + {/* Options */} +
+ {/* Claude option */} + + + {/* Manual option */} + +
+ + {initializerStatus === 'starting' && ( +
+ + Starting agent... +
+ )} + + {initializerError && ( +
+

Failed to start agent

+

{initializerError}

+ +
+ )} +
+ ) +} diff --git a/ui/src/components/ResetProjectModal.tsx b/ui/src/components/ResetProjectModal.tsx new file mode 100644 index 00000000..a17022a6 --- /dev/null +++ b/ui/src/components/ResetProjectModal.tsx @@ -0,0 +1,175 @@ +import { useState } from 'react' +import { X, AlertTriangle, Loader2, RotateCcw, Trash2 } from 'lucide-react' +import { useResetProject } from '../hooks/useProjects' + +interface ResetProjectModalProps { + projectName: string + onClose: () => void + onReset?: () => void +} + +export function ResetProjectModal({ projectName, onClose, onReset }: ResetProjectModalProps) { + const [error, setError] = useState(null) + const [fullReset, setFullReset] = useState(false) + const resetProject = useResetProject() + + const handleReset = async () => { + setError(null) + try { + await resetProject.mutateAsync({ name: projectName, fullReset }) + onReset?.() + onClose() + } catch (err) { + setError(err instanceof Error ? err.message : 'Failed to reset project') + } + } + + return ( +
+
e.stopPropagation()} + > + {/* Header */} +
+

+ + Reset Project +

+ +
+ + {/* Content */} +
+ {/* Error Message */} + {error && ( +
+ + {error} + +
+ )} + +

+ Reset {projectName} to start fresh. +

+ + {/* Reset Type Toggle */} +
+ + + +
+ + {/* What will be deleted */} +
+

This will delete:

+
    +
  • All features and their progress
  • +
  • Assistant chat history
  • +
  • Agent settings
  • + {fullReset && ( +
  • Prompts directory (app_spec.txt, templates)
  • + )} +
+
+ + {/* What will be preserved */} +
+

This will preserve:

+
    + {!fullReset && ( + <> +
  • App spec (prompts/app_spec.txt)
  • +
  • Prompt templates
  • + + )} +
  • Project registration
  • + {fullReset && ( +
  • + (You'll see the setup wizard to create a new spec) +
  • + )} +
+
+ + {/* Actions */} +
+ + +
+
+
+
+ ) +} diff --git a/ui/src/hooks/useProjects.ts b/ui/src/hooks/useProjects.ts index 0cb61fa5..1c0381f2 100644 --- a/ui/src/hooks/useProjects.ts +++ b/ui/src/hooks/useProjects.ts @@ -48,6 +48,21 @@ export function useDeleteProject() { }) } +export function useResetProject() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: ({ name, fullReset = false }: { name: string; fullReset?: boolean }) => + api.resetProject(name, fullReset), + onSuccess: (_, { name }) => { + // Invalidate both projects and features queries + queryClient.invalidateQueries({ queryKey: ['projects'] }) + queryClient.invalidateQueries({ queryKey: ['features', name] }) + queryClient.invalidateQueries({ queryKey: ['project', name] }) + }, + }) +} + // ============================================================================ // Features // ============================================================================ diff --git a/ui/src/lib/api.ts b/ui/src/lib/api.ts index bfee6cc9..f0222431 100644 --- a/ui/src/lib/api.ts +++ b/ui/src/lib/api.ts @@ -66,6 +66,20 @@ export async function deleteProject(name: string): Promise { }) } +export interface ResetProjectResponse { + success: boolean + message: string + deleted_files: string[] + full_reset: boolean +} + +export async function resetProject(name: string, fullReset: boolean = false): Promise { + const params = fullReset ? '?full_reset=true' : '' + return fetchJSON(`/projects/${encodeURIComponent(name)}/reset${params}`, { + method: 'POST', + }) +} + export async function getProjectPrompts(name: string): Promise { return fetchJSON(`/projects/${encodeURIComponent(name)}/prompts`) } diff --git a/ui/tsconfig.tsbuildinfo b/ui/tsconfig.tsbuildinfo index 8bf9c84a..133eb726 100644 --- a/ui/tsconfig.tsbuildinfo +++ b/ui/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/addfeatureform.tsx","./src/components/agentcontrol.tsx","./src/components/agentthought.tsx","./src/components/assistantchat.tsx","./src/components/assistantfab.tsx","./src/components/assistantpanel.tsx","./src/components/chatmessage.tsx","./src/components/debuglogviewer.tsx","./src/components/featurecard.tsx","./src/components/featuremodal.tsx","./src/components/folderbrowser.tsx","./src/components/kanbanboard.tsx","./src/components/kanbancolumn.tsx","./src/components/newprojectmodal.tsx","./src/components/progressdashboard.tsx","./src/components/projectselector.tsx","./src/components/questionoptions.tsx","./src/components/setupwizard.tsx","./src/components/speccreationchat.tsx","./src/components/typingindicator.tsx","./src/hooks/useassistantchat.ts","./src/hooks/usecelebration.ts","./src/hooks/usefeaturesound.ts","./src/hooks/useprojects.ts","./src/hooks/usespecchat.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/types.ts"],"version":"5.6.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/addfeatureform.tsx","./src/components/agentcontrol.tsx","./src/components/agentthought.tsx","./src/components/assistantchat.tsx","./src/components/assistantfab.tsx","./src/components/assistantpanel.tsx","./src/components/chatmessage.tsx","./src/components/debuglogviewer.tsx","./src/components/featurecard.tsx","./src/components/featuremodal.tsx","./src/components/folderbrowser.tsx","./src/components/kanbanboard.tsx","./src/components/kanbancolumn.tsx","./src/components/newprojectmodal.tsx","./src/components/progressdashboard.tsx","./src/components/projectselector.tsx","./src/components/questionoptions.tsx","./src/components/resetprojectmodal.tsx","./src/components/setupwizard.tsx","./src/components/speccreationchat.tsx","./src/components/typingindicator.tsx","./src/hooks/useassistantchat.ts","./src/hooks/usecelebration.ts","./src/hooks/usefeaturesound.ts","./src/hooks/useprojects.ts","./src/hooks/usespecchat.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/types.ts"],"version":"5.6.3"} \ No newline at end of file