const [assistantOpen, setAssistantOpen] = useState(false)
  const [gitPanelOpen, setGitPanelOpen] = useState(false)
  const [terminalOpen, setTerminalOpen] = useState(false)

import { useState, useEffect, useCallback } from 'react'
import { useProjects, useFeatures, useAgentStatus } from './hooks/useProjects'
import { useProjectWebSocket } from './hooks/useWebSocket'
import { useFeatureSound } from './hooks/useFeatureSound'
import { useCelebration } from './hooks/useCelebration'

const STORAGE_KEY = 'autocoder-selected-project'
import { ProjectSelector } from './components/ProjectSelector'
import { KanbanBoard } from './components/KanbanBoard'
import { AgentControl } from './components/AgentControl'
import { ProgressDashboard } from './components/ProgressDashboard'
import { SetupWizard } from './components/SetupWizard'
import { AddFeatureForm } from './components/AddFeatureForm'
import { FeatureModal } from './components/FeatureModal'
import { DebugLogViewer } from './components/DebugLogViewer'
import { AgentThought } from './components/AgentThought'
import { AssistantFAB } from './components/AssistantFAB'
import { AssistantPanel } from './components/AssistantPanel'
import { GitPanel } from './components/GitPanel'
import { TerminalChat } from './components/TerminalChat'
import { Plus, Loader2, GitBranch, Terminal, X } from 'lucide-react'
import type { Feature } from './lib/types'

function App() {
  // Initialize selected project from localStorage
  const [selectedProject, setSelectedProject] = useState<string | null>(() => {
    try {
      return localStorage.getItem(STORAGE_KEY)
    } catch {
      return null
    }
  })
  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 [debugPanelHeight, setDebugPanelHeight] = useState(288) // Default height
  const [assistantOpen, setAssistantOpen] = useState(false)
  const [gitPanelOpen, setGitPanelOpen] = useState(false)

  const { data: projects, isLoading: projectsLoading } = useProjects()
  const { data: features } = useFeatures(selectedProject)
  const { data: agentStatusData } = useAgentStatus(selectedProject)
  const wsState = useProjectWebSocket(selectedProject)

  // Play sounds when features move between columns
  useFeatureSound(features)

  // Celebrate when all features are complete
  useCelebration(features, selectedProject)

  // Persist selected project to localStorage
  const handleSelectProject = useCallback((project: string | null) => {
    setSelectedProject(project)
    try {
      if (project) {
        localStorage.setItem(STORAGE_KEY, project)
      } else {
        localStorage.removeItem(STORAGE_KEY)
      }
    } catch {
      // localStorage not available
    }
  }, [])

  // Validate stored project exists (clear if project was deleted)
  useEffect(() => {
    if (selectedProject && projects && !projects.some(p => p.name === selectedProject)) {
      handleSelectProject(null)
    }
  }, [selectedProject, projects, handleSelectProject])

  // Keyboard shortcuts
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      // Ignore if user is typing in an input
      if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
        return
      }

      // D : Toggle debug window
      if (e.key === 'd' || e.key === 'D') {
        e.preventDefault()
        setDebugOpen(prev => !prev)
      }

      // N : Add new feature (when project selected)
      if ((e.key === 'n' || e.key === 'N') && selectedProject) {
        e.preventDefault()
        setShowAddFeature(true)
      }

      // A : Toggle assistant panel (when project selected)
      if ((e.key === 'a' || e.key === 'A') && selectedProject) {
        e.preventDefault()
        setAssistantOpen(prev => !prev)
      }

      // G : Toggle git panel (when project selected)
      if ((e.key === 'g' || e.key === 'G') && selectedProject) {
        e.preventDefault()
        setGitPanelOpen(prev => !prev)
      }

      // Escape : Close modals
      if (e.key === 'Escape') {
        if (gitPanelOpen) {
          setGitPanelOpen(false)
        } else if (assistantOpen) {
          setAssistantOpen(false)
        } else if (showAddFeature) {
          setShowAddFeature(false)
        } else if (selectedFeature) {
          setSelectedFeature(null)
        } else if (debugOpen) {
          setDebugOpen(false)
        }
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [selectedProject, showAddFeature, selectedFeature, debugOpen, assistantOpen, gitPanelOpen])

  // Combine WebSocket progress with feature data
  const progress = wsState.progress.total > 0 ? wsState.progress : {
    passing: features?.done.length ?? 0,
    total: (features?.pending.length ?? 0) + (features?.in_progress.length ?? 0) + (features?.done.length ?? 0),
    percentage: 0,
  }

  if (progress.total > 0 && progress.percentage === 0) {
    progress.percentage = Math.round((progress.passing / progress.total) * 100 * 10) / 10
  }

  if (!setupComplete) {
    return <SetupWizard onComplete={() => setSetupComplete(true)} />
  }

  return (

      {/* Header */}

            {/* Logo and Title */}

              AutoCoder

            {/* Controls */}

              <ProjectSelector
                projects={projects ?? []}
                selectedProject={selectedProject}
                onSelectProject={handleSelectProject}
                isLoading={projectsLoading}
              />

              {selectedProject && (
                <>
                  <button
                    onClick={() => setShowAddFeature(true)}
                    className="neo-btn neo-btn-primary text-sm"
                    title="Press N"
                  >
                    <Plus size={18} />
                    Add Feature

                      N

                  </button>

                  <button
                    onClick={() => setGitPanelOpen(true)}
                    className="neo-btn text-sm"
                    title="Press G"
                  >
                    <GitBranch size={18} />
                    Git

                      G

                  </button>

                  <AgentControl
                    projectName={selectedProject}
                    status={wsState.agentStatus}
                    yoloMode={agentStatusData?.yolo_mode ?? false}
                  />
                </>
              )}

      {/* Main Content */}

        {!selectedProject ? (

              Welcome to AutoCoder

              Select a project from the dropdown above or create a new one to get started.

        ) : (

            {/* Progress Dashboard */}
            <ProgressDashboard
              passing={progress.passing}
              total={progress.total}
              percentage={progress.percentage}
              isConnected={wsState.isConnected}
            />

            {/* Agent Thought - shows latest agent narrative */}
            <AgentThought
              logs={wsState.logs}
              agentStatus={wsState.agentStatus}
            />

            {/* 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' && (

                <Loader2 size={32} className="animate-spin mx-auto mb-4 text-[var(--color-neo-progress)]" />

                  Initializing Features...

                  The agent is reading your spec and creating features. This may take a moment.

            )}

            {/* Kanban Board */}
            <KanbanBoard
              features={features}
              onFeatureClick={setSelectedFeature}
            />

        )}

      {/* Add Feature Modal */}
      {showAddFeature && selectedProject && (
        <AddFeatureForm
          projectName={selectedProject}
          onClose={() => setShowAddFeature(false)}
        />
      )}

      {/* Feature Detail Modal */}
      {selectedFeature && selectedProject && (
        <FeatureModal
          feature={selectedFeature}
          projectName={selectedProject}
          onClose={() => setSelectedFeature(null)}
        />
      )}

      {/* Debug Log Viewer - fixed to bottom */}
      {selectedProject && (
        <DebugLogViewer
          logs={wsState.logs}
          isOpen={debugOpen}
          onToggle={() => setDebugOpen(!debugOpen)}
          onClear={wsState.clearLogs}
          onHeightChange={setDebugPanelHeight}
        />
      )}

      {/* Assistant FAB and Panel */}
      {selectedProject && (
        <>
          <AssistantFAB
            onClick={() => setAssistantOpen(!assistantOpen)}
            isOpen={assistantOpen}
          />
          <AssistantPanel
            projectName={selectedProject}
            isOpen={assistantOpen}
            onClose={() => setAssistantOpen(false)}
          />
        </>
      )}

      {/* Git Panel */}
      {selectedProject && (
        <GitPanel
          projectName={selectedProject}
          isOpen={gitPanelOpen}
          onClose={() => setGitPanelOpen(false)}
        />
      )}

  )
}

export default App

+  const [terminalOpen, setTerminalOpen] = useState(false)


Now add the keyboard shortcut for terminal (T key).


Edit

// G : Toggle git panel (when project selected)
      if ((e.key === 'g' || e.key === 'G') && selectedProject) {
        e.preventDefault()
        setGitPanelOpen(prev => !prev)
      }

      // Escape : Close modals

// G : Toggle git panel (when project selected)
      if ((e.key === 'g' || e.key === 'G') && selectedProject) {
        e.preventDefault()
        setGitPanelOpen(prev => !prev)
      }

      // T : Toggle terminal panel (when project selected)
      if ((e.key === 't' || e.key === 'T') && selectedProject) {
        e.preventDefault()
        setTerminalOpen(prev => !prev)
      }

      // Escape : Close modals


The file C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\App.tsx has been updated successfully.

// G : Toggle git panel (when project selected)
      if ((e.key === 'g' || e.key === 'G') && selectedProject) {
        e.preventDefault()
        setGitPanelOpen(prev => !prev)
      }

      // T : Toggle terminal panel (when project selected)
      if ((e.key === 't' || e.key === 'T') && selectedProject) {
        e.preventDefault()
        setTerminalOpen(prev => !prev)
      }

      // Escape : Close modals

import { useState, useEffect, useCallback } from 'react'
import { useProjects, useFeatures, useAgentStatus } from './hooks/useProjects'
import { useProjectWebSocket } from './hooks/useWebSocket'
import { useFeatureSound } from './hooks/useFeatureSound'
import { useCelebration } from './hooks/useCelebration'

const STORAGE_KEY = 'autocoder-selected-project'
import { ProjectSelector } from './components/ProjectSelector'
import { KanbanBoard } from './components/KanbanBoard'
import { AgentControl } from './components/AgentControl'
import { ProgressDashboard } from './components/ProgressDashboard'
import { SetupWizard } from './components/SetupWizard'
import { AddFeatureForm } from './components/AddFeatureForm'
import { FeatureModal } from './components/FeatureModal'
import { DebugLogViewer } from './components/DebugLogViewer'
import { AgentThought } from './components/AgentThought'
import { AssistantFAB } from './components/AssistantFAB'
import { AssistantPanel } from './components/AssistantPanel'
import { GitPanel } from './components/GitPanel'
import { TerminalChat } from './components/TerminalChat'
import { Plus, Loader2, GitBranch, Terminal, X } from 'lucide-react'
import type { Feature } from './lib/types'

function App() {
  // Initialize selected project from localStorage
  const [selectedProject, setSelectedProject] = useState<string | null>(() => {
    try {
      return localStorage.getItem(STORAGE_KEY)
    } catch {
      return null
    }
  })
  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 [debugPanelHeight, setDebugPanelHeight] = useState(288) // Default height
  const [assistantOpen, setAssistantOpen] = useState(false)
  const [gitPanelOpen, setGitPanelOpen] = useState(false)
  const [terminalOpen, setTerminalOpen] = useState(false)

  const { data: projects, isLoading: projectsLoading } = useProjects()
  const { data: features } = useFeatures(selectedProject)
  const { data: agentStatusData } = useAgentStatus(selectedProject)
  const wsState = useProjectWebSocket(selectedProject)

  // Play sounds when features move between columns
  useFeatureSound(features)

  // Celebrate when all features are complete
  useCelebration(features, selectedProject)

  // Persist selected project to localStorage
  const handleSelectProject = useCallback((project: string | null) => {
    setSelectedProject(project)
    try {
      if (project) {
        localStorage.setItem(STORAGE_KEY, project)
      } else {
        localStorage.removeItem(STORAGE_KEY)
      }
    } catch {
      // localStorage not available
    }
  }, [])

  // Validate stored project exists (clear if project was deleted)
  useEffect(() => {
    if (selectedProject && projects && !projects.some(p => p.name === selectedProject)) {
      handleSelectProject(null)
    }
  }, [selectedProject, projects, handleSelectProject])

  // Keyboard shortcuts
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      // Ignore if user is typing in an input
      if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
        return
      }

      // D : Toggle debug window
      if (e.key === 'd' || e.key === 'D') {
        e.preventDefault()
        setDebugOpen(prev => !prev)
      }

      // N : Add new feature (when project selected)
      if ((e.key === 'n' || e.key === 'N') && selectedProject) {
        e.preventDefault()
        setShowAddFeature(true)
      }

      // A : Toggle assistant panel (when project selected)
      if ((e.key === 'a' || e.key === 'A') && selectedProject) {
        e.preventDefault()
        setAssistantOpen(prev => !prev)
      }

      // G : Toggle git panel (when project selected)
      if ((e.key === 'g' || e.key === 'G') && selectedProject) {
        e.preventDefault()
        setGitPanelOpen(prev => !prev)
      }

      // Escape : Close modals
      if (e.key === 'Escape') {
        if (gitPanelOpen) {
          setGitPanelOpen(false)
        } else if (assistantOpen) {
          setAssistantOpen(false)
        } else if (showAddFeature) {
          setShowAddFeature(false)
        } else if (selectedFeature) {
          setSelectedFeature(null)
        } else if (debugOpen) {
          setDebugOpen(false)
        }
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [selectedProject, showAddFeature, selectedFeature, debugOpen, assistantOpen, gitPanelOpen])

  // Combine WebSocket progress with feature data
  const progress = wsState.progress.total > 0 ? wsState.progress : {
    passing: features?.done.length ?? 0,
    total: (features?.pending.length ?? 0) + (features?.in_progress.length ?? 0) + (features?.done.length ?? 0),
    percentage: 0,
  }

  if (progress.total > 0 && progress.percentage === 0) {
    progress.percentage = Math.round((progress.passing / progress.total) * 100 * 10) / 10
  }

  if (!setupComplete) {
    return <SetupWizard onComplete={() => setSetupComplete(true)} />
  }

  return (

      {/* Header */}

            {/* Logo and Title */}

              AutoCoder

            {/* Controls */}

              <ProjectSelector
                projects={projects ?? []}
                selectedProject={selectedProject}
                onSelectProject={handleSelectProject}
                isLoading={projectsLoading}
              />

              {selectedProject && (
                <>
                  <button
                    onClick={() => setShowAddFeature(true)}
                    className="neo-btn neo-btn-primary text-sm"
                    title="Press N"
                  >
                    <Plus size={18} />
                    Add Feature

                      N

                  </button>

                  <button
                    onClick={() => setGitPanelOpen(true)}
                    className="neo-btn text-sm"
                    title="Press G"
                  >
                    <GitBranch size={18} />
                    Git

                      G

                  </button>

                  <AgentControl
                    projectName={selectedProject}
                    status={wsState.agentStatus}
                    yoloMode={agentStatusData?.yolo_mode ?? false}
                  />
                </>
              )}

      {/* Main Content */}

        {!selectedProject ? (

              Welcome to AutoCoder

              Select a project from the dropdown above or create a new one to get started.

        ) : (

            {/* Progress Dashboard */}
            <ProgressDashboard
              passing={progress.passing}
              total={progress.total}
              percentage={progress.percentage}
              isConnected={wsState.isConnected}
            />

            {/* Agent Thought - shows latest agent narrative */}
            <AgentThought
              logs={wsState.logs}
              agentStatus={wsState.agentStatus}
            />

            {/* 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' && (

                <Loader2 size={32} className="animate-spin mx-auto mb-4 text-[var(--color-neo-progress)]" />

                  Initializing Features...

                  The agent is reading your spec and creating features. This may take a moment.

            )}

            {/* Kanban Board */}
            <KanbanBoard
              features={features}
              onFeatureClick={setSelectedFeature}
            />

        )}

      {/* Add Feature Modal */}
      {showAddFeature && selectedProject && (
        <AddFeatureForm
          projectName={selectedProject}
          onClose={() => setShowAddFeature(false)}
        />
      )}

      {/* Feature Detail Modal */}
      {selectedFeature && selectedProject && (
        <FeatureModal
          feature={selectedFeature}
          projectName={selectedProject}
          onClose={() => setSelectedFeature(null)}
        />
      )}

      {/* Debug Log Viewer - fixed to bottom */}
      {selectedProject && (
        <DebugLogViewer
          logs={wsState.logs}
          isOpen={debugOpen}
          onToggle={() => setDebugOpen(!debugOpen)}
          onClear={wsState.clearLogs}
          onHeightChange={setDebugPanelHeight}
        />
      )}

      {/* Assistant FAB and Panel */}
      {selectedProject && (
        <>
          <AssistantFAB
            onClick={() => setAssistantOpen(!assistantOpen)}
            isOpen={assistantOpen}
          />
          <AssistantPanel
            projectName={selectedProject}
            isOpen={assistantOpen}
            onClose={() => setAssistantOpen(false)}
          />
        </>
      )}

      {/* Git Panel */}
      {selectedProject && (
        <GitPanel
          projectName={selectedProject}
          isOpen={gitPanelOpen}
          onClose={() => setGitPanelOpen(false)}
        />
      )}

  )
}

export default App

+      // T : Toggle terminal panel (when project selected)

+      if ((e.key === 't' || e.key === 'T') && selectedProject) {


Now update the Escape handler to include terminal and add terminal to the dependency array.


// Escape : Close modals
      if (e.key === 'Escape') {
        if (gitPanelOpen) {
          setGitPanelOpen(false)
        } else if (assistantOpen) {
          setAssistantOpen(false)
        } else if (showAddFeature) {
          setShowAddFeature(false)
        } else if (selectedFeature) {
          setSelectedFeature(null)
        } else if (debugOpen) {
          setDebugOpen(false)
        }
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [selectedProject, showAddFeature, selectedFeature, debugOpen, assistantOpen, gitPanelOpen])

// Escape : Close modals
      if (e.key === 'Escape') {
        if (terminalOpen) {
          setTerminalOpen(false)
        } else if (gitPanelOpen) {
          setGitPanelOpen(false)
        } else if (assistantOpen) {
          setAssistantOpen(false)
        } else if (showAddFeature) {
          setShowAddFeature(false)
        } else if (selectedFeature) {
          setSelectedFeature(null)
        } else if (debugOpen) {
          setDebugOpen(false)
        }
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [selectedProject, showAddFeature, selectedFeature, debugOpen, assistantOpen, gitPanelOpen, terminalOpen])


// Escape : Close modals
      if (e.key === 'Escape') {
        if (terminalOpen) {
          setTerminalOpen(false)
        } else if (gitPanelOpen) {
          setGitPanelOpen(false)
        } else if (assistantOpen) {
          setAssistantOpen(false)
        } else if (showAddFeature) {
          setShowAddFeature(false)
        } else if (selectedFeature) {
          setSelectedFeature(null)
        } else if (debugOpen) {
          setDebugOpen(false)
        }
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [selectedProject, showAddFeature, selectedFeature, debugOpen, assistantOpen, gitPanelOpen, terminalOpen])

import { useState, useEffect, useCallback } from 'react'
import { useProjects, useFeatures, useAgentStatus } from './hooks/useProjects'
import { useProjectWebSocket } from './hooks/useWebSocket'
import { useFeatureSound } from './hooks/useFeatureSound'
import { useCelebration } from './hooks/useCelebration'

const STORAGE_KEY = 'autocoder-selected-project'
import { ProjectSelector } from './components/ProjectSelector'
import { KanbanBoard } from './components/KanbanBoard'
import { AgentControl } from './components/AgentControl'
import { ProgressDashboard } from './components/ProgressDashboard'
import { SetupWizard } from './components/SetupWizard'
import { AddFeatureForm } from './components/AddFeatureForm'
import { FeatureModal } from './components/FeatureModal'
import { DebugLogViewer } from './components/DebugLogViewer'
import { AgentThought } from './components/AgentThought'
import { AssistantFAB } from './components/AssistantFAB'
import { AssistantPanel } from './components/AssistantPanel'
import { GitPanel } from './components/GitPanel'
import { TerminalChat } from './components/TerminalChat'
import { Plus, Loader2, GitBranch, Terminal, X } from 'lucide-react'
import type { Feature } from './lib/types'

function App() {
  // Initialize selected project from localStorage
  const [selectedProject, setSelectedProject] = useState<string | null>(() => {
    try {
      return localStorage.getItem(STORAGE_KEY)
    } catch {
      return null
    }
  })
  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 [debugPanelHeight, setDebugPanelHeight] = useState(288) // Default height
  const [assistantOpen, setAssistantOpen] = useState(false)
  const [gitPanelOpen, setGitPanelOpen] = useState(false)
  const [terminalOpen, setTerminalOpen] = useState(false)

  const { data: projects, isLoading: projectsLoading } = useProjects()
  const { data: features } = useFeatures(selectedProject)
  const { data: agentStatusData } = useAgentStatus(selectedProject)
  const wsState = useProjectWebSocket(selectedProject)

  // Play sounds when features move between columns
  useFeatureSound(features)

  // Celebrate when all features are complete
  useCelebration(features, selectedProject)

  // Persist selected project to localStorage
  const handleSelectProject = useCallback((project: string | null) => {
    setSelectedProject(project)
    try {
      if (project) {
        localStorage.setItem(STORAGE_KEY, project)
      } else {
        localStorage.removeItem(STORAGE_KEY)
      }
    } catch {
      // localStorage not available
    }
  }, [])

  // Validate stored project exists (clear if project was deleted)
  useEffect(() => {
    if (selectedProject && projects && !projects.some(p => p.name === selectedProject)) {
      handleSelectProject(null)
    }
  }, [selectedProject, projects, handleSelectProject])

  // Keyboard shortcuts
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      // Ignore if user is typing in an input
      if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
        return
      }

      // D : Toggle debug window
      if (e.key === 'd' || e.key === 'D') {
        e.preventDefault()
        setDebugOpen(prev => !prev)
      }

      // N : Add new feature (when project selected)
      if ((e.key === 'n' || e.key === 'N') && selectedProject) {
        e.preventDefault()
        setShowAddFeature(true)
      }

      // A : Toggle assistant panel (when project selected)
      if ((e.key === 'a' || e.key === 'A') && selectedProject) {
        e.preventDefault()
        setAssistantOpen(prev => !prev)
      }

      // G : Toggle git panel (when project selected)
      if ((e.key === 'g' || e.key === 'G') && selectedProject) {
        e.preventDefault()
        setGitPanelOpen(prev => !prev)
      }

      // T : Toggle terminal panel (when project selected)
      if ((e.key === 't' || e.key === 'T') && selectedProject) {
        e.preventDefault()
        setTerminalOpen(prev => !prev)
      }

      // Escape : Close modals
      if (e.key === 'Escape') {
        if (gitPanelOpen) {
          setGitPanelOpen(false)
        } else if (assistantOpen) {
          setAssistantOpen(false)
        } else if (showAddFeature) {
          setShowAddFeature(false)
        } else if (selectedFeature) {
          setSelectedFeature(null)
        } else if (debugOpen) {
          setDebugOpen(false)
        }
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [selectedProject, showAddFeature, selectedFeature, debugOpen, assistantOpen, gitPanelOpen])

  // Combine WebSocket progress with feature data
  const progress = wsState.progress.total > 0 ? wsState.progress : {
    passing: features?.done.length ?? 0,
    total: (features?.pending.length ?? 0) + (features?.in_progress.length ?? 0) + (features?.done.length ?? 0),
    percentage: 0,
  }

  if (progress.total > 0 && progress.percentage === 0) {
    progress.percentage = Math.round((progress.passing / progress.total) * 100 * 10) / 10
  }

  if (!setupComplete) {
    return <SetupWizard onComplete={() => setSetupComplete(true)} />
  }

  return (

      {/* Header */}

            {/* Logo and Title */}

              AutoCoder

            {/* Controls */}

              <ProjectSelector
                projects={projects ?? []}
                selectedProject={selectedProject}
                onSelectProject={handleSelectProject}
                isLoading={projectsLoading}
              />

              {selectedProject && (
                <>
                  <button
                    onClick={() => setShowAddFeature(true)}
                    className="neo-btn neo-btn-primary text-sm"
                    title="Press N"
                  >
                    <Plus size={18} />
                    Add Feature

                      N

                  </button>

                  <button
                    onClick={() => setGitPanelOpen(true)}
                    className="neo-btn text-sm"
                    title="Press G"
                  >
                    <GitBranch size={18} />
                    Git

                      G

                  </button>

                  <AgentControl
                    projectName={selectedProject}
                    status={wsState.agentStatus}
                    yoloMode={agentStatusData?.yolo_mode ?? false}
                  />
                </>
              )}

      {/* Main Content */}

        {!selectedProject ? (

              Welcome to AutoCoder

              Select a project from the dropdown above or create a new one to get started.

        ) : (

            {/* Progress Dashboard */}
            <ProgressDashboard
              passing={progress.passing}
              total={progress.total}
              percentage={progress.percentage}
              isConnected={wsState.isConnected}
            />

            {/* Agent Thought - shows latest agent narrative */}
            <AgentThought
              logs={wsState.logs}
              agentStatus={wsState.agentStatus}
            />

            {/* 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' && (

                <Loader2 size={32} className="animate-spin mx-auto mb-4 text-[var(--color-neo-progress)]" />

                  Initializing Features...

                  The agent is reading your spec and creating features. This may take a moment.

            )}

            {/* Kanban Board */}
            <KanbanBoard
              features={features}
              onFeatureClick={setSelectedFeature}
            />

        )}

      {/* Add Feature Modal */}
      {showAddFeature && selectedProject && (
        <AddFeatureForm
          projectName={selectedProject}
          onClose={() => setShowAddFeature(false)}
        />
      )}

      {/* Feature Detail Modal */}
      {selectedFeature && selectedProject && (
        <FeatureModal
          feature={selectedFeature}
          projectName={selectedProject}
          onClose={() => setSelectedFeature(null)}
        />
      )}

      {/* Debug Log Viewer - fixed to bottom */}
      {selectedProject && (
        <DebugLogViewer
          logs={wsState.logs}
          isOpen={debugOpen}
          onToggle={() => setDebugOpen(!debugOpen)}
          onClear={wsState.clearLogs}
          onHeightChange={setDebugPanelHeight}
        />
      )}

      {/* Assistant FAB and Panel */}
      {selectedProject && (
        <>
          <AssistantFAB
            onClick={() => setAssistantOpen(!assistantOpen)}
            isOpen={assistantOpen}
          />
          <AssistantPanel
            projectName={selectedProject}
            isOpen={assistantOpen}
            onClose={() => setAssistantOpen(false)}
          />
        </>
      )}

      {/* Git Panel */}
      {selectedProject && (
        <GitPanel
          projectName={selectedProject}
          isOpen={gitPanelOpen}
          onClose={() => setGitPanelOpen(false)}
        />
      )}

  )
}

export default App

// Escape : Close modals
if (e.key === 'Escape') {
-        if (gitPanelOpen) {
+        if (terminalOpen) {
+          setTerminalOpen(false)
+        } else if (gitPanelOpen) {
setGitPanelOpen(false)
} else if (assistantOpen) {
setAssistantOpen(false)

-  }, [selectedProject, showAddFeature, selectedFeature, debugOpen, assistantOpen, gitPanelOpen])

+  }, [selectedProject, showAddFeature, selectedFeature, debugOpen, assistantOpen, gitPanelOpen, terminalOpen])


Now let me add the Terminal button in the header (before Git button).


<button
                    onClick={() => setGitPanelOpen(true)}
                    className="neo-btn text-sm"
                    title="Press G"
                  >
                    <GitBranch size={18} />
                    Git
                    <kbd className="ml-1.5 px-1.5 py-0.5 text-xs bg-black/20 rounded font-mono">
                      G
                    </kbd>
                  </button>

                  <AgentControl

<button
                    onClick={() => setTerminalOpen(true)}
                    className="neo-btn text-sm"
                    title="Press T"
                  >
                    <Terminal size={18} />
                    Terminal
                    <kbd className="ml-1.5 px-1.5 py-0.5 text-xs bg-black/20 rounded font-mono">
                      T
                    </kbd>
                  </button>

                  <button
                    onClick={() => setGitPanelOpen(true)}
                    className="neo-btn text-sm"
                    title="Press G"
                  >
                    <GitBranch size={18} />
                    Git
                    <kbd className="ml-1.5 px-1.5 py-0.5 text-xs bg-black/20 rounded font-mono">
                      G
                    </kbd>
                  </button>

                  <AgentControl


<button
                    onClick={() => setTerminalOpen(true)}
                    className="neo-btn text-sm"
                    title="Press T"
                  >
                    <Terminal size={18} />
                    Terminal
                    <kbd className="ml-1.5 px-1.5 py-0.5 text-xs bg-black/20 rounded font-mono">
                      T
                    </kbd>
                  </button>

                  <button
                    onClick={() => setGitPanelOpen(true)}
                    className="neo-btn text-sm"
                    title="Press G"
                  >
                    <GitBranch size={18} />
                    Git
                    <kbd className="ml-1.5 px-1.5 py-0.5 text-xs bg-black/20 rounded font-mono">
                      G
                    </kbd>
                  </button>

                  <AgentControl

import { useState, useEffect, useCallback } from 'react'
import { useProjects, useFeatures, useAgentStatus } from './hooks/useProjects'
import { useProjectWebSocket } from './hooks/useWebSocket'
import { useFeatureSound } from './hooks/useFeatureSound'
import { useCelebration } from './hooks/useCelebration'

const STORAGE_KEY = 'autocoder-selected-project'
import { ProjectSelector } from './components/ProjectSelector'
import { KanbanBoard } from './components/KanbanBoard'
import { AgentControl } from './components/AgentControl'
import { ProgressDashboard } from './components/ProgressDashboard'
import { SetupWizard } from './components/SetupWizard'
import { AddFeatureForm } from './components/AddFeatureForm'
import { FeatureModal } from './components/FeatureModal'
import { DebugLogViewer } from './components/DebugLogViewer'
import { AgentThought } from './components/AgentThought'
import { AssistantFAB } from './components/AssistantFAB'
import { AssistantPanel } from './components/AssistantPanel'
import { GitPanel } from './components/GitPanel'
import { TerminalChat } from './components/TerminalChat'
import { Plus, Loader2, GitBranch, Terminal, X } from 'lucide-react'
import type { Feature } from './lib/types'

function App() {
  // Initialize selected project from localStorage
  const [selectedProject, setSelectedProject] = useState<string | null>(() => {
    try {
      return localStorage.getItem(STORAGE_KEY)
    } catch {
      return null
    }
  })
  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 [debugPanelHeight, setDebugPanelHeight] = useState(288) // Default height
  const [assistantOpen, setAssistantOpen] = useState(false)
  const [gitPanelOpen, setGitPanelOpen] = useState(false)
  const [terminalOpen, setTerminalOpen] = useState(false)

  const { data: projects, isLoading: projectsLoading } = useProjects()
  const { data: features } = useFeatures(selectedProject)
  const { data: agentStatusData } = useAgentStatus(selectedProject)
  const wsState = useProjectWebSocket(selectedProject)

  // Play sounds when features move between columns
  useFeatureSound(features)

  // Celebrate when all features are complete
  useCelebration(features, selectedProject)

  // Persist selected project to localStorage
  const handleSelectProject = useCallback((project: string | null) => {
    setSelectedProject(project)
    try {
      if (project) {
        localStorage.setItem(STORAGE_KEY, project)
      } else {
        localStorage.removeItem(STORAGE_KEY)
      }
    } catch {
      // localStorage not available
    }
  }, [])

  // Validate stored project exists (clear if project was deleted)
  useEffect(() => {
    if (selectedProject && projects && !projects.some(p => p.name === selectedProject)) {
      handleSelectProject(null)
    }
  }, [selectedProject, projects, handleSelectProject])

  // Keyboard shortcuts
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      // Ignore if user is typing in an input
      if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
        return
      }

      // D : Toggle debug window
      if (e.key === 'd' || e.key === 'D') {
        e.preventDefault()
        setDebugOpen(prev => !prev)
      }

      // N : Add new feature (when project selected)
      if ((e.key === 'n' || e.key === 'N') && selectedProject) {
        e.preventDefault()
        setShowAddFeature(true)
      }

      // A : Toggle assistant panel (when project selected)
      if ((e.key === 'a' || e.key === 'A') && selectedProject) {
        e.preventDefault()
        setAssistantOpen(prev => !prev)
      }

      // G : Toggle git panel (when project selected)
      if ((e.key === 'g' || e.key === 'G') && selectedProject) {
        e.preventDefault()
        setGitPanelOpen(prev => !prev)
      }

      // T : Toggle terminal panel (when project selected)
      if ((e.key === 't' || e.key === 'T') && selectedProject) {
        e.preventDefault()
        setTerminalOpen(prev => !prev)
      }

      // Escape : Close modals
      if (e.key === 'Escape') {
        if (terminalOpen) {
          setTerminalOpen(false)
        } else if (gitPanelOpen) {
          setGitPanelOpen(false)
        } else if (assistantOpen) {
          setAssistantOpen(false)
        } else if (showAddFeature) {
          setShowAddFeature(false)
        } else if (selectedFeature) {
          setSelectedFeature(null)
        } else if (debugOpen) {
          setDebugOpen(false)
        }
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [selectedProject, showAddFeature, selectedFeature, debugOpen, assistantOpen, gitPanelOpen, terminalOpen])

  // Combine WebSocket progress with feature data
  const progress = wsState.progress.total > 0 ? wsState.progress : {
    passing: features?.done.length ?? 0,
    total: (features?.pending.length ?? 0) + (features?.in_progress.length ?? 0) + (features?.done.length ?? 0),
    percentage: 0,
  }

  if (progress.total > 0 && progress.percentage === 0) {
    progress.percentage = Math.round((progress.passing / progress.total) * 100 * 10) / 10
  }

  if (!setupComplete) {
    return <SetupWizard onComplete={() => setSetupComplete(true)} />
  }

  return (

      {/* Header */}

            {/* Logo and Title */}

              AutoCoder

            {/* Controls */}

              <ProjectSelector
                projects={projects ?? []}
                selectedProject={selectedProject}
                onSelectProject={handleSelectProject}
                isLoading={projectsLoading}
              />

              {selectedProject && (
                <>
                  <button
                    onClick={() => setShowAddFeature(true)}
                    className="neo-btn neo-btn-primary text-sm"
                    title="Press N"
                  >
                    <Plus size={18} />
                    Add Feature

                      N

                  </button>

                  <button
                    onClick={() => setGitPanelOpen(true)}
                    className="neo-btn text-sm"
                    title="Press G"
                  >
                    <GitBranch size={18} />
                    Git

                      G

                  </button>

                  <AgentControl
                    projectName={selectedProject}
                    status={wsState.agentStatus}
                    yoloMode={agentStatusData?.yolo_mode ?? false}
                  />
                </>
              )}

      {/* Main Content */}

        {!selectedProject ? (

              Welcome to AutoCoder

              Select a project from the dropdown above or create a new one to get started.

        ) : (

            {/* Progress Dashboard */}
            <ProgressDashboard
              passing={progress.passing}
              total={progress.total}
              percentage={progress.percentage}
              isConnected={wsState.isConnected}
            />

            {/* Agent Thought - shows latest agent narrative */}
            <AgentThought
              logs={wsState.logs}
              agentStatus={wsState.agentStatus}
            />

            {/* 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' && (

                <Loader2 size={32} className="animate-spin mx-auto mb-4 text-[var(--color-neo-progress)]" />

                  Initializing Features...

                  The agent is reading your spec and creating features. This may take a moment.

            )}

            {/* Kanban Board */}
            <KanbanBoard
              features={features}
              onFeatureClick={setSelectedFeature}
            />

        )}

      {/* Add Feature Modal */}
      {showAddFeature && selectedProject && (
        <AddFeatureForm
          projectName={selectedProject}
          onClose={() => setShowAddFeature(false)}
        />
      )}

      {/* Feature Detail Modal */}
      {selectedFeature && selectedProject && (
        <FeatureModal
          feature={selectedFeature}
          projectName={selectedProject}
          onClose={() => setSelectedFeature(null)}
        />
      )}

      {/* Debug Log Viewer - fixed to bottom */}
      {selectedProject && (
        <DebugLogViewer
          logs={wsState.logs}
          isOpen={debugOpen}
          onToggle={() => setDebugOpen(!debugOpen)}
          onClear={wsState.clearLogs}
          onHeightChange={setDebugPanelHeight}
        />
      )}

      {/* Assistant FAB and Panel */}
      {selectedProject && (
        <>
          <AssistantFAB
            onClick={() => setAssistantOpen(!assistantOpen)}
            isOpen={assistantOpen}
          />
          <AssistantPanel
            projectName={selectedProject}
            isOpen={assistantOpen}
            onClose={() => setAssistantOpen(false)}
          />
        </>
      )}

      {/* Git Panel */}
      {selectedProject && (
        <GitPanel
          projectName={selectedProject}
          isOpen={gitPanelOpen}
          onClose={() => setGitPanelOpen(false)}
        />
      )}

  )
}

export default App

+                    onClick={() => setTerminalOpen(true)}

+                    className="neo-btn text-sm"

+                    <Terminal size={18} />

+                    <kbd className="ml-1.5 px-1.5 py-0.5 text-xs bg-black/20 rounded font-mono">


Now let me add the Terminal panel at the end of the component (similar to GitPanel).


{/* Git Panel */}
      {selectedProject && (
        <GitPanel
          projectName={selectedProject}
          isOpen={gitPanelOpen}
          onClose={() => setGitPanelOpen(false)}
        />
      )}
    </div>
  )

{/* Git Panel */}
      {selectedProject && (
        <GitPanel
          projectName={selectedProject}
          isOpen={gitPanelOpen}
          onClose={() => setGitPanelOpen(false)}
        />
      )}

      {/* Terminal Panel */}
      {selectedProject && (
        <>
          {/* Backdrop */}
          {terminalOpen && (

 setTerminalOpen(false)}
              aria-hidden="true"
            />
          )}
          {/* Panel */}
          <div
            className={`
              fixed right-0 top-0 bottom-0 z-50
              w-[600px] max-w-[90vw]
              bg-white
              border-l-4 border-[var(--color-neo-border)]
              shadow-[-8px_0_0px_rgba(0,0,0,1)]
              transform transition-transform duration-300 ease-out
              flex flex-col
              ${terminalOpen ? 'translate-x-0' : 'translate-x-full'}
            `}
            role="dialog"
            aria-label="Claude Terminal"
            aria-hidden={!terminalOpen}
          >
            {/* Header */}

                  <Terminal size={18} />

Claude Terminal

{selectedProject}

              <button
                onClick={() => setTerminalOpen(false)}
                className="neo-btn neo-btn-ghost p-2 bg-white/20 border-white/40 hover:bg-white/30 text-white"
                title="Close Terminal (Press T)"
              >
                <X size={18} />
              </button>

            {/* Terminal Chat area */}

              {terminalOpen && <TerminalChat projectName={selectedProject} />}

        </>
      )}

  )


{/* Git Panel */}
      {selectedProject && (
        <GitPanel
          projectName={selectedProject}
          isOpen={gitPanelOpen}
          onClose={() => setGitPanelOpen(false)}
        />
      )}
    </div>
  )

{/* Git Panel */}
      {selectedProject && (
        <GitPanel
          projectName={selectedProject}
          isOpen={gitPanelOpen}
          onClose={() => setGitPanelOpen(false)}
        />
      )}

      {/* Terminal Panel */}
      {selectedProject && (
        <>
          {/* Backdrop */}
          {terminalOpen && (

 setTerminalOpen(false)}
              aria-hidden="true"
            />
          )}
          {/* Panel */}
          <div
            className={`
              fixed right-0 top-0 bottom-0 z-50
              w-[600px] max-w-[90vw]
              bg-white
              border-l-4 border-[var(--color-neo-border)]
              shadow-[-8px_0_0px_rgba(0,0,0,1)]
              transform transition-transform duration-300 ease-out
              flex flex-col
              ${terminalOpen ? 'translate-x-0' : 'translate-x-full'}
            `}
            role="dialog"
            aria-label="Claude Terminal"
            aria-hidden={!terminalOpen}
          >
            {/* Header */}

                  <Terminal size={18} />

Claude Terminal

{selectedProject}

              <button
                onClick={() => setTerminalOpen(false)}
                className="neo-btn neo-btn-ghost p-2 bg-white/20 border-white/40 hover:bg-white/30 text-white"
                title="Close Terminal (Press T)"
              >
                <X size={18} />
              </button>

            {/* Terminal Chat area */}

              {terminalOpen && <TerminalChat projectName={selectedProject} />}

        </>
      )}

  )

import { useState, useEffect, useCallback } from 'react'
import { useProjects, useFeatures, useAgentStatus } from './hooks/useProjects'
import { useProjectWebSocket } from './hooks/useWebSocket'
import { useFeatureSound } from './hooks/useFeatureSound'
import { useCelebration } from './hooks/useCelebration'

const STORAGE_KEY = 'autocoder-selected-project'
import { ProjectSelector } from './components/ProjectSelector'
import { KanbanBoard } from './components/KanbanBoard'
import { AgentControl } from './components/AgentControl'
import { ProgressDashboard } from './components/ProgressDashboard'
import { SetupWizard } from './components/SetupWizard'
import { AddFeatureForm } from './components/AddFeatureForm'
import { FeatureModal } from './components/FeatureModal'
import { DebugLogViewer } from './components/DebugLogViewer'
import { AgentThought } from './components/AgentThought'
import { AssistantFAB } from './components/AssistantFAB'
import { AssistantPanel } from './components/AssistantPanel'
import { GitPanel } from './components/GitPanel'
import { TerminalChat } from './components/TerminalChat'
import { Plus, Loader2, GitBranch, Terminal, X } from 'lucide-react'
import type { Feature } from './lib/types'

function App() {
  // Initialize selected project from localStorage
  const [selectedProject, setSelectedProject] = useState<string | null>(() => {
    try {
      return localStorage.getItem(STORAGE_KEY)
    } catch {
      return null
    }
  })
  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 [debugPanelHeight, setDebugPanelHeight] = useState(288) // Default height
  const [assistantOpen, setAssistantOpen] = useState(false)
  const [gitPanelOpen, setGitPanelOpen] = useState(false)
  const [terminalOpen, setTerminalOpen] = useState(false)

  const { data: projects, isLoading: projectsLoading } = useProjects()
  const { data: features } = useFeatures(selectedProject)
  const { data: agentStatusData } = useAgentStatus(selectedProject)
  const wsState = useProjectWebSocket(selectedProject)

  // Play sounds when features move between columns
  useFeatureSound(features)

  // Celebrate when all features are complete
  useCelebration(features, selectedProject)

  // Persist selected project to localStorage
  const handleSelectProject = useCallback((project: string | null) => {
    setSelectedProject(project)
    try {
      if (project) {
        localStorage.setItem(STORAGE_KEY, project)
      } else {
        localStorage.removeItem(STORAGE_KEY)
      }
    } catch {
      // localStorage not available
    }
  }, [])

  // Validate stored project exists (clear if project was deleted)
  useEffect(() => {
    if (selectedProject && projects && !projects.some(p => p.name === selectedProject)) {
      handleSelectProject(null)
    }
  }, [selectedProject, projects, handleSelectProject])

  // Keyboard shortcuts
  useEffect(() => {
    const handleKeyDown = (e: KeyboardEvent) => {
      // Ignore if user is typing in an input
      if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
        return
      }

      // D : Toggle debug window
      if (e.key === 'd' || e.key === 'D') {
        e.preventDefault()
        setDebugOpen(prev => !prev)
      }

      // N : Add new feature (when project selected)
      if ((e.key === 'n' || e.key === 'N') && selectedProject) {
        e.preventDefault()
        setShowAddFeature(true)
      }

      // A : Toggle assistant panel (when project selected)
      if ((e.key === 'a' || e.key === 'A') && selectedProject) {
        e.preventDefault()
        setAssistantOpen(prev => !prev)
      }

      // G : Toggle git panel (when project selected)
      if ((e.key === 'g' || e.key === 'G') && selectedProject) {
        e.preventDefault()
        setGitPanelOpen(prev => !prev)
      }

      // T : Toggle terminal panel (when project selected)
      if ((e.key === 't' || e.key === 'T') && selectedProject) {
        e.preventDefault()
        setTerminalOpen(prev => !prev)
      }

      // Escape : Close modals
      if (e.key === 'Escape') {
        if (terminalOpen) {
          setTerminalOpen(false)
        } else if (gitPanelOpen) {
          setGitPanelOpen(false)
        } else if (assistantOpen) {
          setAssistantOpen(false)
        } else if (showAddFeature) {
          setShowAddFeature(false)
        } else if (selectedFeature) {
          setSelectedFeature(null)
        } else if (debugOpen) {
          setDebugOpen(false)
        }
      }
    }

    window.addEventListener('keydown', handleKeyDown)
    return () => window.removeEventListener('keydown', handleKeyDown)
  }, [selectedProject, showAddFeature, selectedFeature, debugOpen, assistantOpen, gitPanelOpen, terminalOpen])

  // Combine WebSocket progress with feature data
  const progress = wsState.progress.total > 0 ? wsState.progress : {
    passing: features?.done.length ?? 0,
    total: (features?.pending.length ?? 0) + (features?.in_progress.length ?? 0) + (features?.done.length ?? 0),
    percentage: 0,
  }

  if (progress.total > 0 && progress.percentage === 0) {
    progress.percentage = Math.round((progress.passing / progress.total) * 100 * 10) / 10
  }

  if (!setupComplete) {
    return <SetupWizard onComplete={() => setSetupComplete(true)} />
  }

  return (

      {/* Header */}

            {/* Logo and Title */}

              AutoCoder

            {/* Controls */}

              <ProjectSelector
                projects={projects ?? []}
                selectedProject={selectedProject}
                onSelectProject={handleSelectProject}
                isLoading={projectsLoading}
              />

              {selectedProject && (
                <>
                  <button
                    onClick={() => setShowAddFeature(true)}
                    className="neo-btn neo-btn-primary text-sm"
                    title="Press N"
                  >
                    <Plus size={18} />
                    Add Feature

                      N

                  </button>

                  <button
                    onClick={() => setTerminalOpen(true)}
                    className="neo-btn text-sm"
                    title="Press T"
                  >
                    <Terminal size={18} />
                    Terminal

                      T

                  </button>

                  <button
                    onClick={() => setGitPanelOpen(true)}
                    className="neo-btn text-sm"
                    title="Press G"
                  >
                    <GitBranch size={18} />
                    Git

                      G

                  </button>

                  <AgentControl
                    projectName={selectedProject}
                    status={wsState.agentStatus}
                    yoloMode={agentStatusData?.yolo_mode ?? false}
                  />
                </>
              )}

      {/* Main Content */}

        {!selectedProject ? (

              Welcome to AutoCoder

              Select a project from the dropdown above or create a new one to get started.

        ) : (

            {/* Progress Dashboard */}
            <ProgressDashboard
              passing={progress.passing}
              total={progress.total}
              percentage={progress.percentage}
              isConnected={wsState.isConnected}
            />

            {/* Agent Thought - shows latest agent narrative */}
            <AgentThought
              logs={wsState.logs}
              agentStatus={wsState.agentStatus}
            />

            {/* 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' && (

                <Loader2 size={32} className="animate-spin mx-auto mb-4 text-[var(--color-neo-progress)]" />

                  Initializing Features...

                  The agent is reading your spec and creating features. This may take a moment.

            )}

            {/* Kanban Board */}
            <KanbanBoard
              features={features}
              onFeatureClick={setSelectedFeature}
            />

        )}

      {/* Add Feature Modal */}
      {showAddFeature && selectedProject && (
        <AddFeatureForm
          projectName={selectedProject}
          onClose={() => setShowAddFeature(false)}
        />
      )}

      {/* Feature Detail Modal */}
      {selectedFeature && selectedProject && (
        <FeatureModal
          feature={selectedFeature}
          projectName={selectedProject}
          onClose={() => setSelectedFeature(null)}
        />
      )}

      {/* Debug Log Viewer - fixed to bottom */}
      {selectedProject && (
        <DebugLogViewer
          logs={wsState.logs}
          isOpen={debugOpen}
          onToggle={() => setDebugOpen(!debugOpen)}
          onClear={wsState.clearLogs}
          onHeightChange={setDebugPanelHeight}
        />
      )}

      {/* Assistant FAB and Panel */}
      {selectedProject && (
        <>
          <AssistantFAB
            onClick={() => setAssistantOpen(!assistantOpen)}
            isOpen={assistantOpen}
          />
          <AssistantPanel
            projectName={selectedProject}
            isOpen={assistantOpen}
            onClose={() => setAssistantOpen(false)}
          />
        </>
      )}

      {/* Git Panel */}
      {selectedProject && (
        <GitPanel
          projectName={selectedProject}
          isOpen={gitPanelOpen}
          onClose={() => setGitPanelOpen(false)}
        />
      )}

  )
}

export default App

+              className="fixed inset-0 bg-black/20 z-40 transition-opacity duration-300"

+              onClick={() => setTerminalOpen(false)}

+              fixed right-0 top-0 bottom-0 z-50

+              border-l-4 border-[var(--color-neo-border)]

+              transform transition-transform duration-300 ease-out

+              ${terminalOpen ? 'translate-x-0' : 'translate-x-full'}

+            aria-label="Claude Terminal"

+            aria-hidden={!terminalOpen}

+            <div className="flex items-center justify-between px-4 py-3 border-b-3 border-[var(--color-neo-border)] bg-[var(--color-neo-accent)]">

+              <div className="flex items-center gap-2">

+                <div className="bg-white border-2 border-[var(--color-neo-border)] p-1.5 shadow-[2px_2px_0px_rgba(0,0,0,1)]">

+                  <Terminal size={18} />

+                className="neo-btn neo-btn-ghost p-2 bg-white/20 border-white/40 hover:bg-white/30 text-white"

+                title="Close Terminal (Press T)"

+            <div className="flex-1 overflow-hidden">

+              {terminalOpen && <TerminalChat projectName={selectedProject} />}


Now let me create the configuration file schema and default config. I'll create a config.py module for managing configuration.


Now let me create the configuration module.


Write

"""
Autocoder Configuration
=======================

Centralized configuration management for Autocoder 2.0.
Loads settings from ~/.autocoder/config.json with sensible defaults.
"""

import json
import logging
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Optional

logger = logging.getLogger(__name__)

# Default config location
CONFIG_DIR = Path.home() / ".autocoder"
CONFIG_FILE = CONFIG_DIR / "config.json"

@dataclass
class GitAgentConfig:
    """Git agent configuration."""
    auto_commit: bool = True
    auto_push: bool = False
    commit_style: str = "conventional"  # "conventional", "simple", "detailed"
    push_on: list[str] = field(default_factory=lambda: ["feature_complete", "session_end"])
    pull_before: list[str] = field(default_factory=lambda: ["code_review", "feature_start"])

@dataclass
class ParallelConfig:
    """Parallel agent configuration."""
    enabled: bool = True
    max_agents: int = 3
    lock_timeout_seconds: int = 3600

@dataclass
class SecurityConfig:
    """Security configuration."""
    sandbox_enabled: bool = False  # Disabled for full system access
    allowed_hosts: list[str] = field(default_factory=list)  # Empty = all allowed

@dataclass
class PathsConfig:
    """Path configuration."""
    credentials_path: str = "~/claude-projects/shared-data/credentials.md"
    global_resources_path: str = "~/.autocoder/global_resources.json"
    session_logs_path: str = "~/claude-projects/session-logs"

@dataclass
class UIConfig:
    """UI configuration."""
    theme: str = "neobrutalism"
    show_agent_thoughts: bool = True
    sound_enabled: bool = True
    celebration_enabled: bool = True

@dataclass
class AutocoderConfig:
    """Main Autocoder configuration."""
    version: str = "2.0.0"
    default_model: str = "claude-sonnet-4-20250514"
    git_agent: GitAgentConfig = field(default_factory=GitAgentConfig)
    parallel: ParallelConfig = field(default_factory=ParallelConfig)
    security: SecurityConfig = field(default_factory=SecurityConfig)
    paths: PathsConfig = field(default_factory=PathsConfig)
    ui: UIConfig = field(default_factory=UIConfig)

    def to_dict(self) -> dict:
        """Convert to dictionary for JSON serialization."""
        return {
            "version": self.version,
            "default_model": self.default_model,
            "git_agent": asdict(self.git_agent),
            "parallel": asdict(self.parallel),
            "security": asdict(self.security),
            "paths": asdict(self.paths),
            "ui": asdict(self.ui),
        }

    @classmethod
    def from_dict(cls, data: dict) -> "AutocoderConfig":
        """Create from dictionary."""
        return cls(
            version=data.get("version", "2.0.0"),
            default_model=data.get("default_model", "claude-sonnet-4-20250514"),
            git_agent=GitAgentConfig(**data.get("git_agent", {})),
            parallel=ParallelConfig(**data.get("parallel", {})),
            security=SecurityConfig(**data.get("security", {})),
            paths=PathsConfig(**data.get("paths", {})),
            ui=UIConfig(**data.get("ui", {})),
        )

# Global config instance
_config: Optional[AutocoderConfig] = None

def get_config() -> AutocoderConfig:
    """
    Get the global configuration instance.

    Loads from ~/.autocoder/config.json if it exists,
    otherwise returns defaults.

    Returns:
        AutocoderConfig instance
    """
    global _config

    if _config is not None:
        return _config

    _config = load_config()
    return _config

def load_config(path: Optional[Path] = None) -> AutocoderConfig:
    """
    Load configuration from file.

    Args:
        path: Optional path to config file (defaults to ~/.autocoder/config.json)

    Returns:
        AutocoderConfig instance
    """
    config_path = path or CONFIG_FILE

    if config_path.exists():
        try:
            with open(config_path, "r", encoding="utf-8") as f:
                data = json.load(f)
            logger.info(f"Loaded config from {config_path}")
            return AutocoderConfig.from_dict(data)
        except Exception as e:
            logger.warning(f"Failed to load config from {config_path}: {e}")
            logger.info("Using default configuration")

    return AutocoderConfig()

def save_config(config: Optional[AutocoderConfig] = None, path: Optional[Path] = None) -> None:
    """
    Save configuration to file.

    Args:
        config: Config to save (defaults to global config)
        path: Path to save to (defaults to ~/.autocoder/config.json)
    """
    config = config or get_config()
    config_path = path or CONFIG_FILE

    # Ensure directory exists
    config_path.parent.mkdir(parents=True, exist_ok=True)

    with open(config_path, "w", encoding="utf-8") as f:
        json.dump(config.to_dict(), f, indent=2)

    logger.info(f"Saved config to {config_path}")

def reset_config() -> AutocoderConfig:
    """
    Reset configuration to defaults.

    Returns:
        New default AutocoderConfig
    """
    global _config
    _config = AutocoderConfig()
    return _config

def update_config(**kwargs) -> AutocoderConfig:
    """
    Update configuration with new values.

    Args:
        **kwargs: Configuration values to update

    Returns:
        Updated AutocoderConfig
    """
    config = get_config()

    for key, value in kwargs.items():
        if hasattr(config, key):
            setattr(config, key, value)

    return config

def ensure_config_dir() -> Path:
    """
    Ensure the config directory exists.

    Returns:
        Path to config directory
    """
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    return CONFIG_DIR

def create_default_config() -> None:
    """Create default configuration file if it doesn't exist."""
    if not CONFIG_FILE.exists():
        ensure_config_dir()
        save_config(AutocoderConfig())
        logger.info(f"Created default config at {CONFIG_FILE}")

# Initialize config directory on import
ensure_config_dir()


File created successfully at: C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\config.py

"""
Autocoder Configuration
=======================

Centralized configuration management for Autocoder 2.0.
Loads settings from ~/.autocoder/config.json with sensible defaults.
"""

import json
import logging
from dataclasses import dataclass, field, asdict
from pathlib import Path
from typing import Optional

logger = logging.getLogger(__name__)

# Default config location
CONFIG_DIR = Path.home() / ".autocoder"
CONFIG_FILE = CONFIG_DIR / "config.json"

@dataclass
class GitAgentConfig:
    """Git agent configuration."""
    auto_commit: bool = True
    auto_push: bool = False
    commit_style: str = "conventional"  # "conventional", "simple", "detailed"
    push_on: list[str] = field(default_factory=lambda: ["feature_complete", "session_end"])
    pull_before: list[str] = field(default_factory=lambda: ["code_review", "feature_start"])

@dataclass
class ParallelConfig:
    """Parallel agent configuration."""
    enabled: bool = True
    max_agents: int = 3
    lock_timeout_seconds: int = 3600

@dataclass
class SecurityConfig:
    """Security configuration."""
    sandbox_enabled: bool = False  # Disabled for full system access
    allowed_hosts: list[str] = field(default_factory=list)  # Empty = all allowed

@dataclass
class PathsConfig:
    """Path configuration."""
    credentials_path: str = "~/claude-projects/shared-data/credentials.md"
    global_resources_path: str = "~/.autocoder/global_resources.json"
    session_logs_path: str = "~/claude-projects/session-logs"

@dataclass
class UIConfig:
    """UI configuration."""
    theme: str = "neobrutalism"
    show_agent_thoughts: bool = True
    sound_enabled: bool = True
    celebration_enabled: bool = True

@dataclass
class AutocoderConfig:
    """Main Autocoder configuration."""
    version: str = "2.0.0"
    default_model: str = "claude-sonnet-4-20250514"
    git_agent: GitAgentConfig = field(default_factory=GitAgentConfig)
    parallel: ParallelConfig = field(default_factory=ParallelConfig)
    security: SecurityConfig = field(default_factory=SecurityConfig)
    paths: PathsConfig = field(default_factory=PathsConfig)
    ui: UIConfig = field(default_factory=UIConfig)

    def to_dict(self) -> dict:
        """Convert to dictionary for JSON serialization."""
        return {
            "version": self.version,
            "default_model": self.default_model,
            "git_agent": asdict(self.git_agent),
            "parallel": asdict(self.parallel),
            "security": asdict(self.security),
            "paths": asdict(self.paths),
            "ui": asdict(self.ui),
        }

    @classmethod
    def from_dict(cls, data: dict) -> "AutocoderConfig":
        """Create from dictionary."""
        return cls(
            version=data.get("version", "2.0.0"),
            default_model=data.get("default_model", "claude-sonnet-4-20250514"),
            git_agent=GitAgentConfig(**data.get("git_agent", {})),
            parallel=ParallelConfig(**data.get("parallel", {})),
            security=SecurityConfig(**data.get("security", {})),
            paths=PathsConfig(**data.get("paths", {})),
            ui=UIConfig(**data.get("ui", {})),
        )

# Global config instance
_config: Optional[AutocoderConfig] = None

def get_config() -> AutocoderConfig:
    """
    Get the global configuration instance.

    Loads from ~/.autocoder/config.json if it exists,
    otherwise returns defaults.

    Returns:
        AutocoderConfig instance
    """
    global _config

    if _config is not None:
        return _config

    _config = load_config()
    return _config

def load_config(path: Optional[Path] = None) -> AutocoderConfig:
    """
    Load configuration from file.

    Args:
        path: Optional path to config file (defaults to ~/.autocoder/config.json)

    Returns:
        AutocoderConfig instance
    """
    config_path = path or CONFIG_FILE

    if config_path.exists():
        try:
            with open(config_path, "r", encoding="utf-8") as f:
                data = json.load(f)
            logger.info(f"Loaded config from {config_path}")
            return AutocoderConfig.from_dict(data)
        except Exception as e:
            logger.warning(f"Failed to load config from {config_path}: {e}")
            logger.info("Using default configuration")

    return AutocoderConfig()

def save_config(config: Optional[AutocoderConfig] = None, path: Optional[Path] = None) -> None:
    """
    Save configuration to file.

    Args:
        config: Config to save (defaults to global config)
        path: Path to save to (defaults to ~/.autocoder/config.json)
    """
    config = config or get_config()
    config_path = path or CONFIG_FILE

    # Ensure directory exists
    config_path.parent.mkdir(parents=True, exist_ok=True)

    with open(config_path, "w", encoding="utf-8") as f:
        json.dump(config.to_dict(), f, indent=2)

    logger.info(f"Saved config to {config_path}")

def reset_config() -> AutocoderConfig:
    """
    Reset configuration to defaults.

    Returns:
        New default AutocoderConfig
    """
    global _config
    _config = AutocoderConfig()
    return _config

def update_config(**kwargs) -> AutocoderConfig:
    """
    Update configuration with new values.

    Args:
        **kwargs: Configuration values to update

    Returns:
        Updated AutocoderConfig
    """
    config = get_config()

    for key, value in kwargs.items():
        if hasattr(config, key):
            setattr(config, key, value)

    return config

def ensure_config_dir() -> Path:
    """
    Ensure the config directory exists.

    Returns:
        Path to config directory
    """
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    return CONFIG_DIR

def create_default_config() -> None:
    """Create default configuration file if it doesn't exist."""
    if not CONFIG_FILE.exists():
        ensure_config_dir()
        save_config(AutocoderConfig())
        logger.info(f"Created default config at {CONFIG_FILE}")

# Initialize config directory on import
ensure_config_dir()


Let me mark Phase 5 as complete and provide a summary to the user of everything that was done.


TodoWrite

Phase 1: Foundation (sandbox, credentials, resources, context injection)

Phase 2: Claude Terminal (backend + frontend)

Phase 3: Git Agent (backend + frontend)

Phase 4: Parallel coding agents

Phase 5: Integration and UI updates


Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable

Phase 1: Foundation (sandbox, credentials, resources, context injection)

Phase 2: Claude Terminal (backend + frontend)

Phase 3: Git Agent (backend + frontend)

Phase 4.1: Create feature_graph.py for dependency tracking

Phase 4.2: Create file_lock.py for concurrent access control

Phase 4.3: Create parallel_agent.py runner

Phase 5: Integration and UI updates

Phase 3: Git Agent (backend + frontend)

Phase 4: Parallel coding agents

Phase 5: Integration and UI updates


All phases are complete. Let me provide a summary of everything that was implemented in the Autocoder 2.0 upgrade.


All phases complete! Here's a summary of the Autocoder 2.0 implementation:

## Implementation Summary

### Phase 1: Foundation
| File | Purpose |
|------|---------|
| `client.py` | Disabled sandbox for full system access |
| `security.py` | Expanded allowed commands (SSH, docker, etc.) |
| `server/services/credential_resolver.py` | Parse credentials.md |
| `server/services/resource_manager.py` | Global + project resource management |
| `prompts.py` | Context injection (infrastructure, credentials) |

### Phase 2: Claude Terminal
| File | Purpose |
|------|---------|
| `server/services/terminal_session.py` | WebSocket Claude session for resource management |
| `server/routers/terminal.py` | REST + WebSocket endpoints |
| `ui/src/hooks/useTerminal.ts` | React hook for terminal |
| `ui/src/components/TerminalChat.tsx` | Terminal UI component |

### Phase 3: Git Agent
| File | Purpose |
|------|---------|
| `server/services/git_operations.py` | Async git command wrappers |
| `server/agents/git_agent.py` | Change tracking and commit coordination |
| `server/routers/git.py` | REST + WebSocket endpoints |
| `ui/src/hooks/useGit.ts` | React hook for git status |
| `ui/src/components/GitPanel.tsx` | Git status panel |

### Phase 4: Parallel Agents
| File | Purpose |
|------|---------|
| `server/services/feature_graph.py` | Feature dependency tracking |
| `server/services/file_lock.py` | Async file locking |
| `parallel_agent.py` | Parallel agent runner CLI |

### Phase 5: Integration
| File | Purpose |
|------|---------|
| `config.py` | Centralized configuration (~/.autocoder/config.json) |
| `ui/src/App.tsx` | Added Terminal (T) and Git (G) buttons |

## Keyboard Shortcuts
- **N** - Add Feature
- **T** - Toggle Terminal
- **G** - Toggle Git Panel
- **A** - Toggle Assistant
- **D** - Toggle Debug Log
- **Esc** - Close panels

## Running Parallel Agents
```bash
python parallel_agent.py --project-dir my-project --max-parallel 3
```


Given that Claude is pretty smart, can we have something that dynamically decides how many agents can/should be run?


The user wants a smarter system that dynamically decides how many agents to run based on various factors. This is a great idea - rather than a fixed `max_parallel`, Claude could analyze:

1. **Feature complexity** - Simple features might allow more parallelism, complex ones less
2. **File overlap predictions** - Features touching different parts of codebase can parallelize better
3. **System resources** - Available memory, CPU, API rate limits
4. **Feature dependencies** - Graph analysis to find independent feature clusters
5. **Historical data** - How long features took, what failed, etc.

I should create an "orchestrator" or "dynamic scheduler" that uses Claude to analyze features and make intelligent decisions about parallelization. This could:

1. Analyze pending features and cluster them by likely file conflicts
2. Estimate complexity based on description and steps
3. Consider the current state (how many in progress, recent failures)
4. Decide optimal parallelism level

Let me create a `smart_orchestrator.py` or update the parallel agent runner with dynamic scheduling.


Great idea. Let me create a smart orchestrator that uses Claude to dynamically decide parallelization based on feature analysis, complexity, and predicted conflicts.


Write

"""
Smart Orchestrator
==================

Uses Claude to dynamically decide how many agents to run in parallel
based on feature analysis, complexity estimation, and conflict prediction.
"""

import asyncio
import json
import logging
from dataclasses import dataclass, field
from pathlib import Path
from typing import Optional

from claude_agent_sdk import ClaudeSDKClient

from parallel_agent import ParallelAgentRunner, AgentResult
from server.services.feature_graph import FeatureGraph, ParallelFeature
from progress import get_feature_stats

logger = logging.getLogger(__name__)

@dataclass
class FeatureCluster:
    """A group of features that can safely run together."""
    features: list[ParallelFeature]
    estimated_parallelism: int
    reasoning: str
    predicted_conflicts: list[str] = field(default_factory=list)
    total_complexity: int = 0

@dataclass
class OrchestrationDecision:
    """Decision about how to run features."""
    recommended_parallelism: int
    features_to_run: list[ParallelFeature]
    reasoning: str
    risk_level: str  # "low", "medium", "high"
    estimated_duration_minutes: int = 0

ANALYSIS_PROMPT = """You are an expert software architect analyzing features for parallel execution.

Given these pending features, analyze them and decide:
1. How many can safely run in parallel
2. Which specific features to run together
3. Risk assessment for parallel execution

Consider:
- Features touching similar files/directories should NOT run together
- UI features can often parallelize with backend features
- Database/schema changes should run alone
- Test features can parallelize with implementation features
- Simple features (1-2 steps) are safer to parallelize than complex ones

## Pending Features

{features_json}

## Current State
- Features in progress: {in_progress_count}
- Features completed: {completed_count}
- Recent failures: {recent_failures}

## Response Format

Respond with a JSON object:
```json
{{
  "recommended_parallelism": <number 1-5>,
  "feature_ids_to_run": [<list of feature IDs to run now>],
  "reasoning": "<brief explanation of your decision>",
  "risk_level": "<low|medium|high>",
  "estimated_minutes": <rough time estimate>,
  "clusters": [
    {{
      "feature_ids": [<ids that can run together>],
      "reason": "<why these are safe together>"
    }}
  ],
  "warnings": ["<any concerns about these features>"]
}}
```

Be conservative - it's better to run fewer in parallel than risk conflicts.
"""

class SmartOrchestrator:
    """
    Intelligent orchestrator that uses Claude to make parallelization decisions.

    Instead of a fixed max_parallel, this orchestrator:
    1. Analyzes pending features using Claude
    2. Clusters features by predicted file conflicts
    3. Estimates complexity and risk
    4. Dynamically adjusts parallelism based on conditions
    """

    def __init__(
        self,
        project_dir: Path,
        model: str = "claude-sonnet-4-20250514",
        min_parallel: int = 1,
        max_parallel: int = 5,
        yolo_mode: bool = False,
    ):
        """
        Initialize the smart orchestrator.

        Args:
            project_dir: Path to the project directory
            model: Claude model for both analysis and coding
            min_parallel: Minimum parallel agents
            max_parallel: Maximum parallel agents (cap)
            yolo_mode: Skip testing if True
        """
        self.project_dir = project_dir
        self.model = model
        self.min_parallel = min_parallel
        self.max_parallel = max_parallel
        self.yolo_mode = yolo_mode

        self.feature_graph = FeatureGraph(project_dir)
        self._recent_failures: list[str] = []
        self._analysis_cache: dict[str, OrchestrationDecision] = {}

    async def analyze_features(
        self,
        features: list[ParallelFeature],
    ) -> OrchestrationDecision:
        """
        Use Claude to analyze features and decide parallelization.

        Args:
            features: List of pending features

        Returns:
            OrchestrationDecision with recommendations
        """
        if not features:
            return OrchestrationDecision(
                recommended_parallelism=0,
                features_to_run=[],
                reasoning="No pending features",
                risk_level="low",
            )

        # Get current stats
        stats = get_feature_stats(self.project_dir) or {}
        in_progress = stats.get("in_progress", 0)
        completed = stats.get("passing", 0)

        # Prepare features for analysis
        features_data = []
        for f in features[:20]:  # Limit to 20 for context
            features_data.append({
                "id": f.id,
                "name": f.name,
                "category": f.category,
                "priority": f.priority,
                "description": f.description[:200],  # Truncate
                "steps_count": len(f.steps),
                "complexity": f.estimated_complexity,
            })

        prompt = ANALYSIS_PROMPT.format(
            features_json=json.dumps(features_data, indent=2),
            in_progress_count=in_progress,
            completed_count=completed,
            recent_failures=", ".join(self._recent_failures[-5:]) or "None",
        )

        # Quick analysis using Claude
        try:
            # Use a lightweight client for analysis
            from anthropic import Anthropic
            client = Anthropic()

            response = client.messages.create(
                model="claude-sonnet-4-20250514",
                max_tokens=1024,
                messages=[{"role": "user", "content": prompt}],
            )

            # Parse response
            response_text = response.content[0].text

            # Extract JSON from response
            json_start = response_text.find("{")
            json_end = response_text.rfind("}") + 1
            if json_start >= 0 and json_end > json_start:
                decision_data = json.loads(response_text[json_start:json_end])
            else:
                raise ValueError("No JSON found in response")

            # Map feature IDs to features
            feature_map = {f.id: f for f in features}
            features_to_run = [
                feature_map[fid]
                for fid in decision_data.get("feature_ids_to_run", [])
                if fid in feature_map
            ]

            # Apply caps
            parallelism = min(
                max(decision_data.get("recommended_parallelism", 1), self.min_parallel),
                self.max_parallel,
            )

            return OrchestrationDecision(
                recommended_parallelism=parallelism,
                features_to_run=features_to_run[:parallelism],
                reasoning=decision_data.get("reasoning", "Analysis complete"),
                risk_level=decision_data.get("risk_level", "medium"),
                estimated_duration_minutes=decision_data.get("estimated_minutes", 30),
            )

        except Exception as e:
            logger.warning(f"Analysis failed, using fallback: {e}")
            return self._fallback_decision(features)

    def _fallback_decision(
        self,
        features: list[ParallelFeature],
    ) -> OrchestrationDecision:
        """
        Fallback decision when Claude analysis fails.

        Uses simple heuristics based on feature categories.
        """
        if not features:
            return OrchestrationDecision(
                recommended_parallelism=0,
                features_to_run=[],
                reasoning="No features available",
                risk_level="low",
            )

        # Group by category
        categories = {}
        for f in features:
            cat = f.category.lower()
            if cat not in categories:
                categories[cat] = []
            categories[cat].append(f)

        # Pick one from each category (up to max)
        selected = []
        for cat, cat_features in categories.items():
            if len(selected) < self.max_parallel:
                selected.append(cat_features[0])

        # If we have room and features left, add more
        remaining = [f for f in features if f not in selected]
        while len(selected) < self.min_parallel and remaining:
            selected.append(remaining.pop(0))

        return OrchestrationDecision(
            recommended_parallelism=len(selected),
            features_to_run=selected,
            reasoning="Fallback: One feature per category",
            risk_level="medium",
        )

    async def run_smart(
        self,
        max_iterations: Optional[int] = None,
    ) -> dict:
        """
        Run the smart orchestration loop.

        Args:
            max_iterations: Maximum iteration cycles

        Returns:
            Summary statistics
        """
        print("\n" + "=" * 70)
        print("  SMART ORCHESTRATOR")
        print("=" * 70)
        print(f"\nProject: {self.project_dir}")
        print(f"Model: {self.model}")
        print(f"Parallelism range: {self.min_parallel}-{self.max_parallel}")
        print()

        total_completed = 0
        total_failed = 0
        iteration = 0

        while True:
            iteration += 1

            if max_iterations and iteration > max_iterations:
                print(f"\nReached max iterations ({max_iterations})")
                break

            # Get available features
            features = self.feature_graph.get_parallelizable_features(
                count=20,  # Get more for analysis
            )

            if not features:
                stats = get_feature_stats(self.project_dir)
                if stats and stats.get("pending", 0) == 0:
                    print("\n All features completed!")
                    break
                print("\nNo parallelizable features, waiting...")
                await asyncio.sleep(5)
                continue

            # Analyze and decide
            print(f"\n--- Iteration {iteration} ---")
            print(f"Analyzing {len(features)} pending features...")

            decision = await self.analyze_features(features)

            print(f"\nDecision: Run {decision.recommended_parallelism} agents in parallel")
            print(f"Risk level: {decision.risk_level}")
            print(f"Reasoning: {decision.reasoning}")
            print(f"\nFeatures selected:")
            for f in decision.features_to_run:
                print(f"  - #{f.id}: {f.name} [{f.category}]")

            if not decision.features_to_run:
                print("\nNo features selected, waiting...")
                await asyncio.sleep(5)
                continue

            # Create runner with dynamic parallelism
            runner = ParallelAgentRunner(
                project_dir=self.project_dir,
                model=self.model,
                max_parallel=decision.recommended_parallelism,
                yolo_mode=self.yolo_mode,
            )

            # Run the selected features
            tasks = []
            for i, feature in enumerate(decision.features_to_run):
                agent_id = f"smart-{iteration}-{i}"
                task = asyncio.create_task(
                    runner.run_single_feature(feature, agent_id)
                )
                tasks.append((feature, task))

            # Wait for completion
            for feature, task in tasks:
                try:
                    result = await task
                    if result.success:
                        total_completed += 1
                        print(f"  Completed: #{feature.id} {feature.name}")
                    else:
                        total_failed += 1
                        self._recent_failures.append(f"#{feature.id}")
                        print(f"  Failed: #{feature.id} - {result.error}")
                except Exception as e:
                    total_failed += 1
                    self._recent_failures.append(f"#{feature.id}")
                    print(f"  Error: #{feature.id} - {e}")

            # Brief delay
            await asyncio.sleep(3)

        return {
            "total_completed": total_completed,
            "total_failed": total_failed,
            "iterations": iteration,
        }

async def run_smart_orchestrator(
    project_dir: Path,
    model: str = "claude-sonnet-4-20250514",
    max_iterations: Optional[int] = None,
    yolo_mode: bool = False,
) -> dict:
    """
    Convenience function to run smart orchestrator.

    Args:
        project_dir: Project directory
        model: Claude model
        max_iterations: Max iterations
        yolo_mode: Skip testing

    Returns:
        Summary statistics
    """
    orchestrator = SmartOrchestrator(
        project_dir=project_dir,
        model=model,
        yolo_mode=yolo_mode,
    )

    return await orchestrator.run_smart(max_iterations=max_iterations)

if __name__ == "__main__":
    import argparse

    parser = argparse.ArgumentParser(description="Smart orchestrator for parallel coding")
    parser.add_argument(
        "--project-dir",
        type=str,
        required=True,
        help="Project directory",
    )
    parser.add_argument(
        "--model",
        type=str,
        default="claude-sonnet-4-20250514",
        help="Claude model",
    )
    parser.add_argument(
        "--max-iterations",
        type=int,
        default=None,
        help="Maximum iterations",
    )
    parser.add_argument(
        "--yolo",
        action="store_true",
        help="YOLO mode (skip testing)",
    )

    args = parser.parse_args()

    project_path = Path(args.project_dir)
    if not project_path.is_absolute():
        try:
            from registry import get_project_path
            resolved = get_project_path(args.project_dir)
            if resolved:
                project_path = resolved
        except ImportError:
            pass

    result = asyncio.run(run_smart_orchestrator(
        project_dir=project_path,
        model=args.model,
        max_iterations=args.max_iterations,
        yolo_mode=args.yolo,
    ))

    print("\n" + "=" * 70)
    print("  ORCHESTRATION COMPLETE")
    print("=" * 70)
    print(f"\nCompleted: {result['total_completed']}")
    print(f"Failed: {result['total_failed']}")
    print(f"Iterations: {result['iterations']}")