Adding features work

This commit is contained in:
Auto
2025-12-30 16:11:08 +02:00
parent 5ffb6a4c5e
commit cb65cfe151
15 changed files with 562 additions and 126 deletions

View File

@@ -8,7 +8,8 @@ import { ProgressDashboard } from './components/ProgressDashboard'
import { SetupWizard } from './components/SetupWizard'
import { AddFeatureForm } from './components/AddFeatureForm'
import { FeatureModal } from './components/FeatureModal'
import { Plus } from 'lucide-react'
import { DebugLogViewer } from './components/DebugLogViewer'
import { Plus, Loader2 } from 'lucide-react'
import type { Feature } from './lib/types'
function App() {
@@ -16,6 +17,7 @@ function App() {
const [showAddFeature, setShowAddFeature] = useState(false)
const [selectedFeature, setSelectedFeature] = useState<Feature | null>(null)
const [setupComplete, setSetupComplete] = useState(true) // Start optimistic
const [debugOpen, setDebugOpen] = useState(false)
const { data: projects, isLoading: projectsLoading } = useProjects()
const { data: features } = useFeatures(selectedProject)
@@ -78,7 +80,7 @@ function App() {
</header>
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 py-8">
<main className={`max-w-7xl mx-auto px-4 py-8 ${debugOpen ? 'pb-80' : ''}`}>
{!selectedProject ? (
<div className="neo-empty-state mt-12">
<h2 className="font-display text-2xl font-bold mb-2">
@@ -98,6 +100,23 @@ function App() {
isConnected={wsState.isConnected}
/>
{/* Initializing Features State - show when agent is running but no features yet */}
{features &&
features.pending.length === 0 &&
features.in_progress.length === 0 &&
features.done.length === 0 &&
wsState.agentStatus === 'running' && (
<div className="neo-card p-8 text-center">
<Loader2 size={32} className="animate-spin mx-auto mb-4 text-[var(--color-neo-progress)]" />
<h3 className="font-display font-bold text-xl mb-2">
Initializing Features...
</h3>
<p className="text-[var(--color-neo-text-secondary)]">
The agent is reading your spec and creating features. This may take a moment.
</p>
</div>
)}
{/* Kanban Board */}
<KanbanBoard
features={features}
@@ -123,6 +142,16 @@ function App() {
onClose={() => setSelectedFeature(null)}
/>
)}
{/* Debug Log Viewer - fixed to bottom */}
{selectedProject && (
<DebugLogViewer
logs={wsState.logs}
isOpen={debugOpen}
onToggle={() => setDebugOpen(!debugOpen)}
onClear={wsState.clearLogs}
/>
)}
</div>
)
}

View File

@@ -0,0 +1,177 @@
/**
* Debug Log Viewer Component
*
* Collapsible panel at the bottom of the screen showing real-time
* agent output (tool calls, results, steps). Similar to browser DevTools.
*/
import { useEffect, useRef, useState } from 'react'
import { ChevronUp, ChevronDown, Trash2, Terminal } from 'lucide-react'
interface DebugLogViewerProps {
logs: Array<{ line: string; timestamp: string }>
isOpen: boolean
onToggle: () => void
onClear: () => void
}
type LogLevel = 'error' | 'warn' | 'debug' | 'info'
export function DebugLogViewer({
logs,
isOpen,
onToggle,
onClear,
}: DebugLogViewerProps) {
const scrollRef = useRef<HTMLDivElement>(null)
const [autoScroll, setAutoScroll] = useState(true)
// Auto-scroll to bottom when new logs arrive (if user hasn't scrolled up)
useEffect(() => {
if (autoScroll && scrollRef.current && isOpen) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight
}
}, [logs, autoScroll, isOpen])
// Detect if user scrolled up
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
const el = e.currentTarget
const isAtBottom = el.scrollHeight - el.scrollTop <= el.clientHeight + 50
setAutoScroll(isAtBottom)
}
// Parse log level from line content
const getLogLevel = (line: string): LogLevel => {
const lowerLine = line.toLowerCase()
if (lowerLine.includes('error') || lowerLine.includes('exception') || lowerLine.includes('traceback')) {
return 'error'
}
if (lowerLine.includes('warn') || lowerLine.includes('warning')) {
return 'warn'
}
if (lowerLine.includes('debug')) {
return 'debug'
}
return 'info'
}
// Get color class for log level
const getLogColor = (level: LogLevel): string => {
switch (level) {
case 'error':
return 'text-red-400'
case 'warn':
return 'text-yellow-400'
case 'debug':
return 'text-gray-400'
case 'info':
default:
return 'text-green-400'
}
}
// Format timestamp to HH:MM:SS
const formatTimestamp = (timestamp: string): string => {
try {
const date = new Date(timestamp)
return date.toLocaleTimeString('en-US', {
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
})
} catch {
return ''
}
}
return (
<div
className={`fixed bottom-0 left-0 right-0 z-40 transition-all duration-200 ${
isOpen ? 'h-72' : 'h-10'
}`}
>
{/* Header bar */}
<div
className="flex items-center justify-between h-10 px-4 bg-[#1a1a1a] border-t-3 border-black cursor-pointer"
onClick={onToggle}
>
<div className="flex items-center gap-2">
<Terminal size={16} className="text-green-400" />
<span className="font-mono text-sm text-white font-bold">
Debug
</span>
{logs.length > 0 && (
<span className="px-2 py-0.5 text-xs font-mono bg-[#333] text-gray-300 rounded">
{logs.length}
</span>
)}
{!autoScroll && isOpen && (
<span className="px-2 py-0.5 text-xs font-mono bg-yellow-600 text-white rounded">
Paused
</span>
)}
</div>
<div className="flex items-center gap-2">
{isOpen && (
<button
onClick={(e) => {
e.stopPropagation()
onClear()
}}
className="p-1.5 hover:bg-[#333] rounded transition-colors"
title="Clear logs"
>
<Trash2 size={14} className="text-gray-400" />
</button>
)}
<div className="p-1">
{isOpen ? (
<ChevronDown size={16} className="text-gray-400" />
) : (
<ChevronUp size={16} className="text-gray-400" />
)}
</div>
</div>
</div>
{/* Log content area */}
{isOpen && (
<div
ref={scrollRef}
onScroll={handleScroll}
className="h-[calc(100%-2.5rem)] overflow-y-auto bg-[#1a1a1a] p-2 font-mono text-sm"
>
{logs.length === 0 ? (
<div className="flex items-center justify-center h-full text-gray-500">
No logs yet. Start the agent to see output.
</div>
) : (
<div className="space-y-0.5">
{logs.map((log, index) => {
const level = getLogLevel(log.line)
const colorClass = getLogColor(level)
const timestamp = formatTimestamp(log.timestamp)
return (
<div
key={`${log.timestamp}-${index}`}
className="flex gap-2 hover:bg-[#2a2a2a] px-1 py-0.5 rounded"
>
<span className="text-gray-500 select-none shrink-0">
{timestamp}
</span>
<span className={`${colorClass} whitespace-pre-wrap break-all`}>
{log.line}
</span>
</div>
)
})}
</div>
)}
</div>
)}
</div>
)
}

View File

@@ -12,6 +12,9 @@ import { useState } from 'react'
import { X, Bot, FileEdit, ArrowRight, ArrowLeft, Loader2, CheckCircle2 } from 'lucide-react'
import { useCreateProject } from '../hooks/useProjects'
import { SpecCreationChat } from './SpecCreationChat'
import { startAgent } from '../lib/api'
type InitializerStatus = 'idle' | 'starting' | 'error'
type Step = 'name' | 'method' | 'chat' | 'complete'
type SpecMethod = 'claude' | 'manual'
@@ -31,6 +34,8 @@ export function NewProjectModal({
const [projectName, setProjectName] = useState('')
const [_specMethod, setSpecMethod] = useState<SpecMethod | null>(null)
const [error, setError] = useState<string | null>(null)
const [initializerStatus, setInitializerStatus] = useState<InitializerStatus>('idle')
const [initializerError, setInitializerError] = useState<string | null>(null)
// Suppress unused variable warning - specMethod may be used in future
void _specMethod
@@ -89,12 +94,27 @@ export function NewProjectModal({
}
}
const handleSpecComplete = () => {
setStep('complete')
setTimeout(() => {
onProjectCreated(projectName.trim())
handleClose()
}, 1500)
const handleSpecComplete = async () => {
// Auto-start the initializer agent
setInitializerStatus('starting')
try {
await startAgent(projectName.trim())
// Success - navigate to project
setStep('complete')
setTimeout(() => {
onProjectCreated(projectName.trim())
handleClose()
}, 1500)
} catch (err) {
setInitializerStatus('error')
setInitializerError(err instanceof Error ? err.message : 'Failed to start agent')
}
}
const handleRetryInitializer = () => {
setInitializerError(null)
setInitializerStatus('idle')
handleSpecComplete()
}
const handleChatCancel = () => {
@@ -108,6 +128,8 @@ export function NewProjectModal({
setProjectName('')
setSpecMethod(null)
setError(null)
setInitializerStatus('idle')
setInitializerError(null)
onClose()
}
@@ -126,6 +148,9 @@ export function NewProjectModal({
projectName={projectName.trim()}
onComplete={handleSpecComplete}
onCancel={handleChatCancel}
initializerStatus={initializerStatus}
initializerError={initializerError}
onRetryInitializer={handleRetryInitializer}
/>
</div>
)

View File

@@ -6,22 +6,30 @@
*/
import { useEffect, useRef, useState } from 'react'
import { Send, X, CheckCircle2, AlertCircle, Wifi, WifiOff, RotateCcw } from 'lucide-react'
import { Send, X, CheckCircle2, AlertCircle, Wifi, WifiOff, RotateCcw, Loader2, ArrowRight } from 'lucide-react'
import { useSpecChat } from '../hooks/useSpecChat'
import { ChatMessage } from './ChatMessage'
import { QuestionOptions } from './QuestionOptions'
import { TypingIndicator } from './TypingIndicator'
type InitializerStatus = 'idle' | 'starting' | 'error'
interface SpecCreationChatProps {
projectName: string
onComplete: (specPath: string) => void
onCancel: () => void
initializerStatus?: InitializerStatus
initializerError?: string | null
onRetryInitializer?: () => void
}
export function SpecCreationChat({
projectName,
onComplete,
onCancel,
initializerStatus = 'idle',
initializerError = null,
onRetryInitializer,
}: SpecCreationChatProps) {
const [input, setInput] = useState('')
const [error, setError] = useState<string | null>(null)
@@ -241,18 +249,50 @@ export function SpecCreationChat({
{/* Completion footer */}
{isComplete && (
<div className="p-4 border-t-3 border-[var(--color-neo-border)] bg-[var(--color-neo-done)]">
<div className={`p-4 border-t-3 border-[var(--color-neo-border)] ${
initializerStatus === 'error' ? 'bg-[var(--color-neo-danger)]' : 'bg-[var(--color-neo-done)]'
}`}>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<CheckCircle2 size={20} />
<span className="font-bold">Specification created successfully!</span>
{initializerStatus === 'starting' ? (
<>
<Loader2 size={20} className="animate-spin" />
<span className="font-bold">Starting agent...</span>
</>
) : initializerStatus === 'error' ? (
<>
<AlertCircle size={20} className="text-white" />
<span className="font-bold text-white">
{initializerError || 'Failed to start agent'}
</span>
</>
) : (
<>
<CheckCircle2 size={20} />
<span className="font-bold">Specification created successfully!</span>
</>
)}
</div>
<div className="flex items-center gap-2">
{initializerStatus === 'error' && onRetryInitializer && (
<button
onClick={onRetryInitializer}
className="neo-btn bg-white"
>
<RotateCcw size={14} />
Retry
</button>
)}
{initializerStatus === 'idle' && (
<button
onClick={() => onComplete('')}
className="neo-btn neo-btn-primary"
>
Continue to Project
<ArrowRight size={16} />
</button>
)}
</div>
<button
onClick={() => onComplete('')}
className="neo-btn bg-white"
>
Continue to Project
</button>
</div>
</div>
)}

View File

@@ -203,7 +203,9 @@ export function useSpecChat({
},
])
onComplete?.(data.path)
// NOTE: Do NOT auto-call onComplete here!
// User should click "Continue to Project" button to start the agent.
// This matches the CLI behavior where user closes the chat manually.
break
}

View File

@@ -1 +1 @@
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/addfeatureform.tsx","./src/components/agentcontrol.tsx","./src/components/chatmessage.tsx","./src/components/featurecard.tsx","./src/components/featuremodal.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/useprojects.ts","./src/hooks/usespecchat.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/types.ts"],"version":"5.6.3"}
{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/addfeatureform.tsx","./src/components/agentcontrol.tsx","./src/components/chatmessage.tsx","./src/components/debuglogviewer.tsx","./src/components/featurecard.tsx","./src/components/featuremodal.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/useprojects.ts","./src/hooks/usespecchat.ts","./src/hooks/usewebsocket.ts","./src/lib/api.ts","./src/lib/types.ts"],"version":"5.6.3"}

View File

@@ -4,7 +4,7 @@ import tailwindcss from '@tailwindcss/vite'
import path from 'path'
// Backend port - can be overridden via VITE_API_PORT env var
const apiPort = process.env.VITE_API_PORT || '8000'
const apiPort = process.env.VITE_API_PORT || '8888'
// https://vite.dev/config/
export default defineConfig({