Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions server/routers/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
6 changes: 6 additions & 0 deletions start_ui.sh
100644 → 100755
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 41 additions & 4 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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)

Expand Down Expand Up @@ -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)
Expand All @@ -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 : {
Expand Down Expand Up @@ -160,6 +171,16 @@ function App() {
</kbd>
</button>

<button
onClick={() => setShowResetModal(true)}
className="neo-btn bg-[var(--color-neo-pending)] text-[var(--color-neo-text)] text-sm"
title="Reset project to start fresh"
disabled={wsState.agentStatus === 'running'}
>
<RotateCcw size={18} />
Reset
</button>

<AgentControl
projectName={selectedProject}
status={wsState.agentStatus}
Expand All @@ -186,6 +207,14 @@ function App() {
Select a project from the dropdown above or create a new one to get started.
</p>
</div>
) : needsSetup ? (
<ProjectSetupRequired
projectName={selectedProject}
onSetupComplete={() => {
// Refetch projects to update has_spec status
refetchProjects()
}}
/>
) : (
<div className="space-y-8">
{/* Progress Dashboard */}
Expand Down Expand Up @@ -245,6 +274,14 @@ function App() {
/>
)}

{/* Reset Project Modal */}
{showResetModal && selectedProject && (
<ResetProjectModal
projectName={selectedProject}
onClose={() => setShowResetModal(false)}
/>
)}

{/* Debug Log Viewer - fixed to bottom */}
{selectedProject && (
<DebugLogViewer
Expand Down
175 changes: 175 additions & 0 deletions ui/src/components/ProjectSetupRequired.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
/**
* Project Setup Required Component
*
* Shown when a project exists but doesn't have a spec file (e.g., after full reset).
* Offers the same options as new project creation: Claude or manual spec.
*/

import { useState } from 'react'
import { Bot, FileEdit, Loader2, AlertTriangle } from 'lucide-react'
import { SpecCreationChat } from './SpecCreationChat'
import { startAgent } from '../lib/api'

type InitializerStatus = 'idle' | 'starting' | 'error'

interface ProjectSetupRequiredProps {
projectName: string
onSetupComplete: () => void
}

export function ProjectSetupRequired({ projectName, onSetupComplete }: ProjectSetupRequiredProps) {
const [showChat, setShowChat] = useState(false)
const [initializerStatus, setInitializerStatus] = useState<InitializerStatus>('idle')
const [initializerError, setInitializerError] = useState<string | null>(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 (
<div className="fixed inset-0 z-50 bg-[var(--color-neo-bg)]">
<SpecCreationChat
projectName={projectName}
onComplete={handleSpecComplete}
onCancel={handleChatCancel}
onExitToProject={handleExitToProject}
initializerStatus={initializerStatus}
initializerError={initializerError}
onRetryInitializer={handleRetryInitializer}
/>
</div>
)
}

return (
<div className="neo-card p-8 max-w-2xl mx-auto mt-12">
{/* Header */}
<div className="flex items-center gap-3 mb-6">
<div className="p-2 bg-[var(--color-neo-warning)] border-3 border-[var(--color-neo-border)]">
<AlertTriangle size={24} />
</div>
<div>
<h2 className="font-display font-bold text-2xl">Setup Required</h2>
<p className="text-[var(--color-neo-text-secondary)]">
Project <strong>{projectName}</strong> needs an app specification to get started.
</p>
</div>
</div>

{/* Options */}
<div className="space-y-4">
{/* Claude option */}
<button
onClick={handleClaudeSelect}
className={`
w-full text-left p-4
border-3 border-[var(--color-neo-border)]
bg-white
shadow-[4px_4px_0px_rgba(0,0,0,1)]
hover:translate-x-[-2px] hover:translate-y-[-2px]
hover:shadow-[6px_6px_0px_rgba(0,0,0,1)]
transition-all duration-150
`}
>
<div className="flex items-start gap-4">
<div className="p-2 bg-[var(--color-neo-progress)] border-2 border-[var(--color-neo-border)] shadow-[2px_2px_0px_rgba(0,0,0,1)]">
<Bot size={24} className="text-white" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-bold text-lg text-[#1a1a1a]">Create with Claude</span>
<span className="neo-badge bg-[var(--color-neo-done)] text-xs">
Recommended
</span>
</div>
<p className="text-sm text-[var(--color-neo-text-secondary)] mt-1">
Interactive conversation to define features and generate your app specification automatically.
</p>
</div>
</div>
</button>

{/* Manual option */}
<button
onClick={handleManualSelect}
className={`
w-full text-left p-4
border-3 border-[var(--color-neo-border)]
bg-white
shadow-[4px_4px_0px_rgba(0,0,0,1)]
hover:translate-x-[-2px] hover:translate-y-[-2px]
hover:shadow-[6px_6px_0px_rgba(0,0,0,1)]
transition-all duration-150
`}
>
<div className="flex items-start gap-4">
<div className="p-2 bg-[var(--color-neo-pending)] border-2 border-[var(--color-neo-border)] shadow-[2px_2px_0px_rgba(0,0,0,1)]">
<FileEdit size={24} />
</div>
<div className="flex-1">
<span className="font-bold text-lg text-[#1a1a1a]">Edit Templates Manually</span>
<p className="text-sm text-[var(--color-neo-text-secondary)] mt-1">
Edit the template files directly in <code className="bg-gray-100 px-1">prompts/app_spec.txt</code>. Best for developers who want full control.
</p>
</div>
</div>
</button>
</div>

{initializerStatus === 'starting' && (
<div className="mt-6 flex items-center justify-center gap-2 text-[var(--color-neo-text-secondary)]">
<Loader2 size={16} className="animate-spin" />
<span>Starting agent...</span>
</div>
)}

{initializerError && (
<div className="mt-6 p-4 bg-[var(--color-neo-danger)] text-white border-3 border-[var(--color-neo-border)]">
<p className="font-bold mb-2">Failed to start agent</p>
<p className="text-sm">{initializerError}</p>
<button
onClick={handleRetryInitializer}
className="mt-3 neo-btn bg-white text-[var(--color-neo-danger)]"
>
Retry
</button>
</div>
)}
</div>
)
}
Loading