feat: Add arbitrary directory project storage with registry system

This major update replaces the fixed `generations/` directory with support
for storing projects in any directory on the filesystem. Projects are now
tracked via a cross-platform registry system.

## New Features

### Project Registry (`registry.py`)
- Cross-platform registry storing project name-to-path mappings
- Platform-specific config locations:
  - Windows: %APPDATA%\autonomous-coder\projects.json
  - macOS: ~/Library/Application Support/autonomous-coder/projects.json
  - Linux: ~/.config/autonomous-coder/projects.json
- POSIX path format for cross-platform compatibility
- File locking for concurrent access safety (fcntl/msvcrt)
- Atomic writes via temp file + rename to prevent corruption
- Fixed Windows file locking issue with tempfile.mkstemp()

### Filesystem Browser API (`server/routers/filesystem.py`)
- REST endpoints for browsing directories server-side
- Cross-platform support with blocked system paths:
  - Windows: C:\Windows, Program Files, ProgramData, etc.
  - macOS: /System, /Library, /private, etc.
  - Linux: /etc, /var, /usr, /bin, etc.
- Universal blocked paths: .ssh, .aws, .gnupg, .docker, etc.
- Hidden file detection (Unix dot-prefix + Windows attributes)
- UNC path blocking for security
- Windows drive enumeration via ctypes
- Directory creation with validation
- Added `has_children` field to DirectoryEntry schema

### UI Folder Browser (`ui/src/components/FolderBrowser.tsx`)
- React component for selecting project directories
- Breadcrumb navigation with clickable segments
- Windows drive selector
- New folder creation inline
- Fixed text visibility with explicit color values

## Updated Components

### Server Routers
- `projects.py`: Uses registry instead of fixed generations/ directory
- `agent.py`: Uses registry for project path lookups
- `features.py`: Uses registry for database path resolution
- `spec_creation.py`: Uses registry for WebSocket project resolution

### Process Manager (`server/services/process_manager.py`)
- Fixed sandbox issue: subprocess now uses project_dir as cwd
- This allows the Claude SDK sandbox to access external project directories

### Schemas (`server/schemas.py`)
- Added `has_children` to DirectoryEntry
- Added `in_progress` to ProjectStats
- Added path field to ProjectSummary and ProjectDetail

### UI Components
- `NewProjectModal.tsx`: Multi-step wizard with folder selection
- Added clarifying text about subfolder creation
- Fixed text color visibility issues

### API Client (`ui/src/lib/api.ts`)
- Added filesystem API functions (listDirectory, createDirectory)
- Fixed Windows path splitting for directory creation

### Documentation
- Updated CLAUDE.md with registry system details
- Updated command examples for absolute paths

## Security Improvements
- Blocked `.` and `..` in directory names to prevent traversal
- Added path blocking check in project creation
- UNC path blocking throughout filesystem API

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Auto
2025-12-31 10:20:07 +02:00
parent 21f737e767
commit 6c99e40408
24 changed files with 2018 additions and 195 deletions

View File

@@ -3,20 +3,22 @@
*
* Multi-step modal for creating new projects:
* 1. Enter project name
* 2. Choose spec method (Claude or manual)
* 3a. If Claude: Show SpecCreationChat
* 3b. If manual: Create project and close
* 2. Select project folder
* 3. Choose spec method (Claude or manual)
* 4a. If Claude: Show SpecCreationChat
* 4b. If manual: Create project and close
*/
import { useState } from 'react'
import { X, Bot, FileEdit, ArrowRight, ArrowLeft, Loader2, CheckCircle2 } from 'lucide-react'
import { X, Bot, FileEdit, ArrowRight, ArrowLeft, Loader2, CheckCircle2, Folder } from 'lucide-react'
import { useCreateProject } from '../hooks/useProjects'
import { SpecCreationChat } from './SpecCreationChat'
import { FolderBrowser } from './FolderBrowser'
import { startAgent } from '../lib/api'
type InitializerStatus = 'idle' | 'starting' | 'error'
type Step = 'name' | 'method' | 'chat' | 'complete'
type Step = 'name' | 'folder' | 'method' | 'chat' | 'complete'
type SpecMethod = 'claude' | 'manual'
interface NewProjectModalProps {
@@ -32,6 +34,7 @@ export function NewProjectModal({
}: NewProjectModalProps) {
const [step, setStep] = useState<Step>('name')
const [projectName, setProjectName] = useState('')
const [projectPath, setProjectPath] = useState<string | null>(null)
const [_specMethod, setSpecMethod] = useState<SpecMethod | null>(null)
const [error, setError] = useState<string | null>(null)
const [initializerStatus, setInitializerStatus] = useState<InitializerStatus>('idle')
@@ -59,17 +62,35 @@ export function NewProjectModal({
}
setError(null)
setStep('folder')
}
const handleFolderSelect = (path: string) => {
// Append project name to the selected path
const fullPath = path.endsWith('/') ? `${path}${projectName.trim()}` : `${path}/${projectName.trim()}`
setProjectPath(fullPath)
setStep('method')
}
const handleFolderCancel = () => {
setStep('name')
}
const handleMethodSelect = async (method: SpecMethod) => {
setSpecMethod(method)
if (!projectPath) {
setError('Please select a project folder first')
setStep('folder')
return
}
if (method === 'manual') {
// Create project immediately with manual method
try {
const project = await createProject.mutateAsync({
name: projectName.trim(),
path: projectPath,
specMethod: 'manual',
})
setStep('complete')
@@ -85,6 +106,7 @@ export function NewProjectModal({
try {
await createProject.mutateAsync({
name: projectName.trim(),
path: projectPath,
specMethod: 'claude',
})
setStep('chat')
@@ -126,6 +148,7 @@ export function NewProjectModal({
const handleClose = () => {
setStep('name')
setProjectName('')
setProjectPath(null)
setSpecMethod(null)
setError(null)
setInitializerStatus('idle')
@@ -135,8 +158,11 @@ export function NewProjectModal({
const handleBack = () => {
if (step === 'method') {
setStep('name')
setStep('folder')
setSpecMethod(null)
} else if (step === 'folder') {
setStep('name')
setProjectPath(null)
}
}
@@ -156,6 +182,47 @@ export function NewProjectModal({
)
}
// Folder step uses larger modal
if (step === 'folder') {
return (
<div className="neo-modal-backdrop" onClick={handleClose}>
<div
className="neo-modal w-full max-w-3xl max-h-[85vh] flex flex-col"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="flex items-center justify-between p-4 border-b-3 border-[var(--color-neo-border)]">
<div className="flex items-center gap-3">
<Folder size={24} className="text-[var(--color-neo-progress)]" />
<div>
<h2 className="font-display font-bold text-xl text-[#1a1a1a]">
Select Project Location
</h2>
<p className="text-sm text-[#4a4a4a]">
A folder named <span className="font-bold font-mono">{projectName}</span> will be created inside the selected directory
</p>
</div>
</div>
<button
onClick={handleClose}
className="neo-btn neo-btn-ghost p-2"
>
<X size={20} />
</button>
</div>
{/* Folder Browser */}
<div className="flex-1 overflow-hidden">
<FolderBrowser
onSelect={handleFolderSelect}
onCancel={handleFolderCancel}
/>
</div>
</div>
</div>
)
}
return (
<div className="neo-modal-backdrop" onClick={handleClose}>
<div