feat: Add "Expand Project" for bulk AI-powered feature creation

Adds the ability to add multiple features to an existing project through
a natural language conversation with Claude, similar to how initial spec
creation works.

Features:
- New "Expand" button in header (keyboard shortcut: E)
- Full-screen chat interface for describing new features
- Claude reads existing app_spec.txt for context
- Features created directly in database after user approval
- Bulk feature creation endpoint for batch operations

New files:
- .claude/commands/expand-project.md - Claude skill for expansion
- server/services/expand_chat_session.py - Chat session service
- server/routers/expand_project.py - WebSocket endpoint
- ui/src/components/ExpandProjectChat.tsx - Chat UI
- ui/src/components/ExpandProjectModal.tsx - Modal wrapper
- ui/src/hooks/useExpandChat.ts - WebSocket hook

Modified:
- Added POST /bulk endpoint to features router
- Added FeatureBulkCreate schemas
- Integrated Expand button and modal in App.tsx

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Dan Gentry
2026-01-09 15:56:01 -05:00
parent 122f03dc21
commit 5f06dcf464
13 changed files with 1863 additions and 6 deletions

View File

@@ -1,4 +1,5 @@
import { useState, useEffect, useCallback } from 'react'
import { useQueryClient } from '@tanstack/react-query'
import { useProjects, useFeatures, useAgentStatus } from './hooks/useProjects'
import { useProjectWebSocket } from './hooks/useWebSocket'
import { useFeatureSound } from './hooks/useFeatureSound'
@@ -16,7 +17,8 @@ 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 { ExpandProjectModal } from './components/ExpandProjectModal'
import { Plus, Loader2, Sparkles } from 'lucide-react'
import type { Feature } from './lib/types'
function App() {
@@ -29,12 +31,14 @@ function App() {
}
})
const [showAddFeature, setShowAddFeature] = useState(false)
const [showExpandProject, setShowExpandProject] = useState(false)
const [selectedFeature, setSelectedFeature] = useState<Feature | null>(null)
const [setupComplete, setSetupComplete] = useState(true) // Start optimistic
const [debugOpen, setDebugOpen] = useState(false)
const [debugPanelHeight, setDebugPanelHeight] = useState(288) // Default height
const [assistantOpen, setAssistantOpen] = useState(false)
const queryClient = useQueryClient()
const { data: projects, isLoading: projectsLoading } = useProjects()
const { data: features } = useFeatures(selectedProject)
const { data: agentStatusData } = useAgentStatus(selectedProject)
@@ -87,6 +91,13 @@ function App() {
setShowAddFeature(true)
}
// E : Expand project with AI (when project selected and has features)
if ((e.key === 'e' || e.key === 'E') && selectedProject && features &&
(features.pending.length + features.in_progress.length + features.done.length) > 0) {
e.preventDefault()
setShowExpandProject(true)
}
// A : Toggle assistant panel (when project selected)
if ((e.key === 'a' || e.key === 'A') && selectedProject) {
e.preventDefault()
@@ -95,7 +106,9 @@ function App() {
// Escape : Close modals
if (e.key === 'Escape') {
if (assistantOpen) {
if (showExpandProject) {
setShowExpandProject(false)
} else if (assistantOpen) {
setAssistantOpen(false)
} else if (showAddFeature) {
setShowAddFeature(false)
@@ -109,7 +122,7 @@ function App() {
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [selectedProject, showAddFeature, selectedFeature, debugOpen, assistantOpen])
}, [selectedProject, showAddFeature, showExpandProject, selectedFeature, debugOpen, assistantOpen, features])
// Combine WebSocket progress with feature data
const progress = wsState.progress.total > 0 ? wsState.progress : {
@@ -160,6 +173,21 @@ function App() {
</kbd>
</button>
{/* Expand Project - only show if project has features */}
{features && (features.pending.length + features.in_progress.length + features.done.length) > 0 && (
<button
onClick={() => setShowExpandProject(true)}
className="neo-btn bg-[var(--color-neo-progress)] text-black text-sm"
title="Add multiple features via AI (Press E)"
>
<Sparkles size={18} />
Expand
<kbd className="ml-1.5 px-1.5 py-0.5 text-xs bg-black/20 rounded font-mono">
E
</kbd>
</button>
)}
<AgentControl
projectName={selectedProject}
status={wsState.agentStatus}
@@ -245,6 +273,19 @@ function App() {
/>
)}
{/* Expand Project Modal - AI-powered bulk feature creation */}
{showExpandProject && selectedProject && (
<ExpandProjectModal
isOpen={showExpandProject}
projectName={selectedProject}
onClose={() => setShowExpandProject(false)}
onFeaturesAdded={() => {
// Invalidate features query to refresh the kanban board
queryClient.invalidateQueries({ queryKey: ['features', selectedProject] })
}}
/>
)}
{/* Debug Log Viewer - fixed to bottom */}
{selectedProject && (
<DebugLogViewer
@@ -256,8 +297,8 @@ function App() {
/>
)}
{/* Assistant FAB and Panel */}
{selectedProject && (
{/* Assistant FAB and Panel - hide FAB when expand modal is open */}
{selectedProject && !showExpandProject && (
<>
<AssistantFAB
onClick={() => setAssistantOpen(!assistantOpen)}