     1→/**
     2→ * Hook for managing terminal WebSocket connection
     3→ *
     4→ * Provides interactive Claude terminal sessions for resource management,
     5→ * infrastructure discovery, and system administration tasks.
     6→ */
     7→
     8→import { useState, useCallback, useRef, useEffect } from 'react'
     9→
    10→type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error'
    11→
    12→export interface TerminalMessage {
    13→  id: string
    14→  role: 'user' | 'assistant' | 'system'
    15→  content: string
    16→  timestamp: Date
    17→  isStreaming?: boolean
    18→  metadata?: {
    19→    command?: string
    20→    resourceUpdated?: string
    21→    connectionVerified?: boolean
    22→  }
    23→}
    24→
    25→export interface Resource {
    26→  name: string
    27→  type: string
    28→  scope: string
    29→  host?: string
    30→  url?: string
    31→  port?: number
    32→  user?: string
    33→  verified_at?: string
    34→  capabilities?: string[]
    35→  description?: string
    36→}
    37→
    38→export interface UseTerminalOptions {
    39→  projectName: string
    40→  onError?: (error: string) => void
    41→  onResourcesUpdated?: (resources: Record<string, Resource>) => void
    42→}
    43→
    44→export interface UseTerminalReturn {
    45→  messages: TerminalMessage[]
    46→  resources: Record<string, Resource>
    47→  isLoading: boolean
    48→  connectionStatus: ConnectionStatus
    49→  start: () => void
    50→  sendMessage: (content: string) => void
    51→  disconnect: () => void
    52→  refreshResources: () => void
    53→}
    54→
    55→function generateId(): string {
    56→  return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
    57→}
    58→
    59→export function useTerminal({
    60→  projectName,
    61→  onError,
    62→  onResourcesUpdated,
    63→}: UseTerminalOptions): UseTerminalReturn {
    64→  const [messages, setMessages] = useState<TerminalMessage[]>([])
    65→  const [resources, setResources] = useState<Record<string, Resource>>({})
    66→  const [isLoading, setIsLoading] = useState(false)
    67→  const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('disconnected')
    68→
    69→  const wsRef = useRef<WebSocket | null>(null)
    70→  const currentAssistantMessageRef = useRef<string | null>(null)
    71→  const reconnectAttempts = useRef(0)
    72→  const maxReconnectAttempts = 3
    73→  const pingIntervalRef = useRef<number | null>(null)
    74→  const reconnectTimeoutRef = useRef<number | null>(null)
    75→
    76→  // Clean up on unmount
    77→  useEffect(() => {
    78→    return () => {
    79→      if (pingIntervalRef.current) {
    80→        clearInterval(pingIntervalRef.current)
    81→      }
    82→      if (reconnectTimeoutRef.current) {
    83→        clearTimeout(reconnectTimeoutRef.current)
    84→      }
    85→      if (wsRef.current) {
    86→        wsRef.current.close()
    87→      }
    88→    }
    89→  }, [])
    90→
    91→  const connect = useCallback(() => {
    92→    if (wsRef.current?.readyState === WebSocket.OPEN) {
    93→      return
    94→    }
    95→
    96→    setConnectionStatus('connecting')
    97→
    98→    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
    99→    const host = window.location.host
   100→    const wsUrl = `${protocol}//${host}/api/terminal/ws/${encodeURIComponent(projectName)}`
   101→
   102→    const ws = new WebSocket(wsUrl)
   103→    wsRef.current = ws
   104→
   105→    ws.onopen = () => {
   106→      setConnectionStatus('connected')
   107→      reconnectAttempts.current = 0
   108→
   109→      // Start ping interval to keep connection alive
   110→      pingIntervalRef.current = window.setInterval(() => {
   111→        if (ws.readyState === WebSocket.OPEN) {
   112→          ws.send(JSON.stringify({ type: 'ping' }))
   113→        }
   114→      }, 30000)
   115→    }
   116→
   117→    ws.onclose = () => {
   118→      setConnectionStatus('disconnected')
   119→      if (pingIntervalRef.current) {
   120→        clearInterval(pingIntervalRef.current)
   121→        pingIntervalRef.current = null
   122→      }
   123→
   124→      // Attempt reconnection if not intentionally closed
   125→      if (reconnectAttempts.current < maxReconnectAttempts) {
   126→        reconnectAttempts.current++
   127→        const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 10000)
   128→        reconnectTimeoutRef.current = window.setTimeout(connect, delay)
   129→      }
   130→    }
   131→
   132→    ws.onerror = () => {
   133→      setConnectionStatus('error')
   134→      onError?.('WebSocket connection error')
   135→    }
   136→
   137→    ws.onmessage = (event) => {
   138→      try {
   139→        const data = JSON.parse(event.data)
   140→
   141→        switch (data.type) {
   142→          case 'text': {
   143→            // Append text to current assistant message or create new one
   144→            setMessages((prev) => {
   145→              const lastMessage = prev[prev.length - 1]
   146→              if (lastMessage?.role === 'assistant' && lastMessage.isStreaming) {
   147→                // Append to existing streaming message
   148→                return [
   149→                  ...prev.slice(0, -1),
   150→                  {
   151→                    ...lastMessage,
   152→                    content: lastMessage.content + data.content,
   153→                  },
   154→                ]
   155→              } else {
   156→                // Create new assistant message
   157→                currentAssistantMessageRef.current = generateId()
   158→                return [
   159→                  ...prev,
   160→                  {
   161→                    id: currentAssistantMessageRef.current,
   162→                    role: 'assistant',
   163→                    content: data.content,
   164→                    timestamp: new Date(),
   165→                    isStreaming: true,
   166→                  },
   167→                ]
   168→              }
   169→            })
   170→            break
   171→          }
   172→
   173→          case 'resources': {
   174→            // Update resources state
   175→            setResources(data.data || {})
   176→            onResourcesUpdated?.(data.data || {})
   177→            break
   178→          }
   179→
   180→          case 'resource_updated': {
   181→            // Resource file was updated - add system message
   182→            setMessages((prev) => [
   183→              ...prev,
   184→              {
   185→                id: generateId(),
   186→                role: 'system',
   187→                content: `Resource file updated: ${data.path}`,
   188→                timestamp: new Date(),
   189→                metadata: { resourceUpdated: data.path },
   190→              },
   191→            ])
   192→            break
   193→          }
   194→
   195→          case 'command_executing': {
   196→            // Show command being executed
   197→            setMessages((prev) => [
   198→              ...prev,
   199→              {
   200→                id: generateId(),
   201→                role: 'system',
   202→                content: `Executing: ${data.command}`,
   203→                timestamp: new Date(),
   204→                metadata: { command: data.command },
   205→              },
   206→            ])
   207→            break
   208→          }
   209→
   210→          case 'connection_verified': {
   211→            // Connection test result
   212→            setMessages((prev) => [
   213→              ...prev,
   214→              {
   215→                id: generateId(),
   216→                role: 'system',
   217→                content: `Connection verified: ${data.result}`,
   218→                timestamp: new Date(),
   219→                metadata: { connectionVerified: true },
   220→              },
   221→            ])
   222→            break
   223→          }
   224→
   225→          case 'response_done': {
   226→            setIsLoading(false)
   227→
   228→            // Mark current message as done streaming
   229→            setMessages((prev) => {
   230→              const lastMessage = prev[prev.length - 1]
   231→              if (lastMessage?.role === 'assistant' && lastMessage.isStreaming) {
   232→                return [
   233→                  ...prev.slice(0, -1),
   234→                  { ...lastMessage, isStreaming: false },
   235→                ]
   236→              }
   237→              return prev
   238→            })
   239→            break
   240→          }
   241→
   242→          case 'error': {
   243→            setIsLoading(false)
   244→            onError?.(data.content)
   245→
   246→            // Add error message
   247→            setMessages((prev) => [
   248→              ...prev,
   249→              {
   250→                id: generateId(),
   251→                role: 'system',
   252→                content: `Error: ${data.content}`,
   253→                timestamp: new Date(),
   254→              },
   255→            ])
   256→            break
   257→          }
   258→
   259→          case 'pong': {
   260→            // Ping response - ignore
   261→            break
   262→          }
   263→
   264→          default: {
   265→            console.warn('Unknown terminal message type:', data.type)
   266→          }
   267→        }
   268→      } catch (e) {
   269→        console.error('Failed to parse terminal message:', e)
   270→      }
   271→    }
   272→  }, [projectName, onError, onResourcesUpdated])
   273→
   274→  const disconnect = useCallback(() => {
   275→    reconnectAttempts.current = maxReconnectAttempts // Prevent auto-reconnect
   276→    if (reconnectTimeoutRef.current) {
   277→      clearTimeout(reconnectTimeoutRef.current)
   278→      reconnectTimeoutRef.current = null
   279→    }
   280→    if (pingIntervalRef.current) {
   281→      clearInterval(pingIntervalRef.current)
   282→      pingIntervalRef.current = null
   283→    }
   284→    if (wsRef.current) {
   285→      wsRef.current.close()
   286→      wsRef.current = null
   287→    }
   288→    setConnectionStatus('disconnected')
   289→  }, [])
   290→
   291→  const start = useCallback(() => {
   292→    connect()
   293→
   294→    // Wait for connection then send start message
   295→    const checkConnection = setInterval(() => {
   296→      if (wsRef.current?.readyState === WebSocket.OPEN) {
   297→        clearInterval(checkConnection)
   298→        setIsLoading(true)
   299→        wsRef.current.send(JSON.stringify({ type: 'start' }))
   300→      }
   301→    }, 100)
   302→
   303→    // Timeout after 10 seconds
   304→    setTimeout(() => {
   305→      clearInterval(checkConnection)
   306→      if (connectionStatus !== 'connected') {
   307→        onError?.('Connection timeout')
   308→      }
   309→    }, 10000)
   310→  }, [connect, connectionStatus, onError])
   311→
   312→  const sendMessage = useCallback((content: string) => {
   313→    if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
   314→      onError?.('Not connected')
   315→      return
   316→    }
   317→
   318→    // Add user message to state
   319→    setMessages((prev) => [
   320→      ...prev,
   321→      {
   322→        id: generateId(),
   323→        role: 'user',
   324→        content,
   325→        timestamp: new Date(),
   326→      },
   327→    ])
   328→
   329→    setIsLoading(true)
   330→    wsRef.current.send(JSON.stringify({ type: 'message', content }))
   331→  }, [onError])
   332→
   333→  const refreshResources = useCallback(() => {
   334→    if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
   335→      return
   336→    }
   337→    wsRef.current.send(JSON.stringify({ type: 'get_resources' }))
   338→  }, [])
   339→
   340→  return {
   341→    messages,
   342→    resources,
   343→    isLoading,
   344→    connectionStatus,
   345→    start,
   346→    sendMessage,
   347→    disconnect,
   348→    refreshResources,
   349→  }
   350→}
   351→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

/**
 * Hook for managing terminal WebSocket connection
 *
 * Provides interactive Claude terminal sessions for resource management,
 * infrastructure discovery, and system administration tasks.
 */

import { useState, useCallback, useRef, useEffect } from 'react'

type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error'

export interface TerminalMessage {
  id: string
  role: 'user' | 'assistant' | 'system'
  content: string
  timestamp: Date
  isStreaming?: boolean
  metadata?: {
    command?: string
    resourceUpdated?: string
    connectionVerified?: boolean
  }
}

export interface Resource {
  name: string
  type: string
  scope: string
  host?: string
  url?: string
  port?: number
  user?: string
  verified_at?: string
  capabilities?: string[]
  description?: string
}

export interface UseTerminalOptions {
  projectName: string
  onError?: (error: string) => void
  onResourcesUpdated?: (resources: Record<string, Resource>) => void
}

export interface UseTerminalReturn {
  messages: TerminalMessage[]
  resources: Record<string, Resource>
  isLoading: boolean
  connectionStatus: ConnectionStatus
  start: () => void
  sendMessage: (content: string) => void
  disconnect: () => void
  refreshResources: () => void
}

function generateId(): string {
  return `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`
}

export function useTerminal({
  projectName,
  onError,
  onResourcesUpdated,
}: UseTerminalOptions): UseTerminalReturn {
  const [messages, setMessages] = useState<TerminalMessage[]>([])
  const [resources, setResources] = useState<Record<string, Resource>>({})
  const [isLoading, setIsLoading] = useState(false)
  const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('disconnected')

  const wsRef = useRef<WebSocket | null>(null)
  const currentAssistantMessageRef = useRef<string | null>(null)
  const reconnectAttempts = useRef(0)
  const maxReconnectAttempts = 3
  const pingIntervalRef = useRef<number | null>(null)
  const reconnectTimeoutRef = useRef<number | null>(null)

  // Clean up on unmount
  useEffect(() => {
    return () => {
      if (pingIntervalRef.current) {
        clearInterval(pingIntervalRef.current)
      }
      if (reconnectTimeoutRef.current) {
        clearTimeout(reconnectTimeoutRef.current)
      }
      if (wsRef.current) {
        wsRef.current.close()
      }
    }
  }, [])

  const connect = useCallback(() => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      return
    }

    setConnectionStatus('connecting')

    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
    const host = window.location.host
    const wsUrl = `${protocol}//${host}/api/terminal/ws/${encodeURIComponent(projectName)}`

    const ws = new WebSocket(wsUrl)
    wsRef.current = ws

    ws.onopen = () => {
      setConnectionStatus('connected')
      reconnectAttempts.current = 0

      // Start ping interval to keep connection alive
      pingIntervalRef.current = window.setInterval(() => {
        if (ws.readyState === WebSocket.OPEN) {
          ws.send(JSON.stringify({ type: 'ping' }))
        }
      }, 30000)
    }

    ws.onclose = () => {
      setConnectionStatus('disconnected')
      if (pingIntervalRef.current) {
        clearInterval(pingIntervalRef.current)
        pingIntervalRef.current = null
      }

      // Attempt reconnection if not intentionally closed
      if (reconnectAttempts.current < maxReconnectAttempts) {
        reconnectAttempts.current++
        const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 10000)
        reconnectTimeoutRef.current = window.setTimeout(connect, delay)
      }
    }

    ws.onerror = () => {
      setConnectionStatus('error')
      onError?.('WebSocket connection error')
    }

    ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data)

        switch (data.type) {
          case 'text': {
            // Append text to current assistant message or create new one
            setMessages((prev) => {
              const lastMessage = prev[prev.length - 1]
              if (lastMessage?.role === 'assistant' && lastMessage.isStreaming) {
                // Append to existing streaming message
                return [
                  ...prev.slice(0, -1),
                  {
                    ...lastMessage,
                    content: lastMessage.content + data.content,
                  },
                ]
              } else {
                // Create new assistant message
                currentAssistantMessageRef.current = generateId()
                return [
                  ...prev,
                  {
                    id: currentAssistantMessageRef.current,
                    role: 'assistant',
                    content: data.content,
                    timestamp: new Date(),
                    isStreaming: true,
                  },
                ]
              }
            })
            break
          }

          case 'resources': {
            // Update resources state
            setResources(data.data || {})
            onResourcesUpdated?.(data.data || {})
            break
          }

          case 'resource_updated': {
            // Resource file was updated - add system message
            setMessages((prev) => [
              ...prev,
              {
                id: generateId(),
                role: 'system',
                content: `Resource file updated: ${data.path}`,
                timestamp: new Date(),
                metadata: { resourceUpdated: data.path },
              },
            ])
            break
          }

          case 'command_executing': {
            // Show command being executed
            setMessages((prev) => [
              ...prev,
              {
                id: generateId(),
                role: 'system',
                content: `Executing: ${data.command}`,
                timestamp: new Date(),
                metadata: { command: data.command },
              },
            ])
            break
          }

          case 'connection_verified': {
            // Connection test result
            setMessages((prev) => [
              ...prev,
              {
                id: generateId(),
                role: 'system',
                content: `Connection verified: ${data.result}`,
                timestamp: new Date(),
                metadata: { connectionVerified: true },
              },
            ])
            break
          }

          case 'response_done': {
            setIsLoading(false)

            // Mark current message as done streaming
            setMessages((prev) => {
              const lastMessage = prev[prev.length - 1]
              if (lastMessage?.role === 'assistant' && lastMessage.isStreaming) {
                return [
                  ...prev.slice(0, -1),
                  { ...lastMessage, isStreaming: false },
                ]
              }
              return prev
            })
            break
          }

          case 'error': {
            setIsLoading(false)
            onError?.(data.content)

            // Add error message
            setMessages((prev) => [
              ...prev,
              {
                id: generateId(),
                role: 'system',
                content: `Error: ${data.content}`,
                timestamp: new Date(),
              },
            ])
            break
          }

          case 'pong': {
            // Ping response - ignore
            break
          }

          default: {
            console.warn('Unknown terminal message type:', data.type)
          }
        }
      } catch (e) {
        console.error('Failed to parse terminal message:', e)
      }
    }
  }, [projectName, onError, onResourcesUpdated])

  const disconnect = useCallback(() => {
    reconnectAttempts.current = maxReconnectAttempts // Prevent auto-reconnect
    if (reconnectTimeoutRef.current) {
      clearTimeout(reconnectTimeoutRef.current)
      reconnectTimeoutRef.current = null
    }
    if (pingIntervalRef.current) {
      clearInterval(pingIntervalRef.current)
      pingIntervalRef.current = null
    }
    if (wsRef.current) {
      wsRef.current.close()
      wsRef.current = null
    }
    setConnectionStatus('disconnected')
  }, [])

  const start = useCallback(() => {
    connect()

    // Wait for connection then send start message
    const checkConnection = setInterval(() => {
      if (wsRef.current?.readyState === WebSocket.OPEN) {
        clearInterval(checkConnection)
        setIsLoading(true)
        wsRef.current.send(JSON.stringify({ type: 'start' }))
      }
    }, 100)

    // Timeout after 10 seconds
    setTimeout(() => {
      clearInterval(checkConnection)
      if (connectionStatus !== 'connected') {
        onError?.('Connection timeout')
      }
    }, 10000)
  }, [connect, connectionStatus, onError])

  const sendMessage = useCallback((content: string) => {
    if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
      onError?.('Not connected')
      return
    }

    // Add user message to state
    setMessages((prev) => [
      ...prev,
      {
        id: generateId(),
        role: 'user',
        content,
        timestamp: new Date(),
      },
    ])

    setIsLoading(true)
    wsRef.current.send(JSON.stringify({ type: 'message', content }))
  }, [onError])

  const refreshResources = useCallback(() => {
    if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
      return
    }
    wsRef.current.send(JSON.stringify({ type: 'get_resources' }))
  }, [])

  return {
    messages,
    resources,
    isLoading,
    connectionStatus,
    start,
    sendMessage,
    disconnect,
    refreshResources,
  }
}


Now I have a good understanding of the patterns. Let me also check the types to see if there are shared type definitions.


Let me check the types file and the globals.css for the design system.


     1→/**
     2→ * TypeScript types for the Autonomous Coding UI
     3→ */
     4→
     5→// Project types
     6→export interface ProjectStats {
     7→  passing: number
     8→  in_progress: number
     9→  total: number
    10→  percentage: number
    11→}
    12→
    13→export interface ProjectSummary {
    14→  name: string
    15→  path: string
    16→  has_spec: boolean
    17→  stats: ProjectStats
    18→}
    19→
    20→export interface ProjectDetail extends ProjectSummary {
    21→  prompts_dir: string
    22→}
    23→
    24→// Filesystem types
    25→export interface DriveInfo {
    26→  letter: string
    27→  label: string
    28→  available?: boolean
    29→}
    30→
    31→export interface DirectoryEntry {
    32→  name: string
    33→  path: string
    34→  is_directory: boolean
    35→  has_children: boolean
    36→}
    37→
    38→export interface DirectoryListResponse {
    39→  current_path: string
    40→  parent_path: string | null
    41→  entries: DirectoryEntry[]
    42→  drives: DriveInfo[] | null
    43→}
    44→
    45→export interface PathValidationResponse {
    46→  valid: boolean
    47→  exists: boolean
    48→  is_directory: boolean
    49→  can_write: boolean
    50→  message: string
    51→}
    52→
    53→export interface ProjectPrompts {
    54→  app_spec: string
    55→  initializer_prompt: string
    56→  coding_prompt: string
    57→}
    58→
    59→// Feature types
    60→export interface Feature {
    61→  id: number
    62→  priority: number
    63→  category: string
    64→  name: string
    65→  description: string
    66→  steps: string[]
    67→  passes: boolean
    68→  in_progress: boolean
    69→}
    70→
    71→export interface FeatureListResponse {
    72→  pending: Feature[]
    73→  in_progress: Feature[]
    74→  done: Feature[]
    75→}
    76→
    77→export interface FeatureCreate {
    78→  category: string
    79→  name: string
    80→  description: string
    81→  steps: string[]
    82→  priority?: number
    83→}
    84→
    85→// Agent types
    86→export type AgentStatus = 'stopped' | 'running' | 'paused' | 'crashed'
    87→
    88→export interface AgentStatusResponse {
    89→  status: AgentStatus
    90→  pid: number | null
    91→  started_at: string | null
    92→  yolo_mode: boolean
    93→}
    94→
    95→export interface AgentActionResponse {
    96→  success: boolean
    97→  status: AgentStatus
    98→  message: string
    99→}
   100→
   101→// Setup types
   102→export interface SetupStatus {
   103→  claude_cli: boolean
   104→  credentials: boolean
   105→  node: boolean
   106→  npm: boolean
   107→}
   108→
   109→// WebSocket message types
   110→export type WSMessageType = 'progress' | 'feature_update' | 'log' | 'agent_status' | 'pong'
   111→
   112→export interface WSProgressMessage {
   113→  type: 'progress'
   114→  passing: number
   115→  in_progress: number
   116→  total: number
   117→  percentage: number
   118→}
   119→
   120→export interface WSFeatureUpdateMessage {
   121→  type: 'feature_update'
   122→  feature_id: number
   123→  passes: boolean
   124→}
   125→
   126→export interface WSLogMessage {
   127→  type: 'log'
   128→  line: string
   129→  timestamp: string
   130→}
   131→
   132→export interface WSAgentStatusMessage {
   133→  type: 'agent_status'
   134→  status: AgentStatus
   135→}
   136→
   137→export interface WSPongMessage {
   138→  type: 'pong'
   139→}
   140→
   141→export type WSMessage =
   142→  | WSProgressMessage
   143→  | WSFeatureUpdateMessage
   144→  | WSLogMessage
   145→  | WSAgentStatusMessage
   146→  | WSPongMessage
   147→
   148→// ============================================================================
   149→// Spec Chat Types
   150→// ============================================================================
   151→
   152→export interface SpecQuestionOption {
   153→  label: string
   154→  description: string
   155→}
   156→
   157→export interface SpecQuestion {
   158→  question: string
   159→  header: string
   160→  options: SpecQuestionOption[]
   161→  multiSelect: boolean
   162→}
   163→
   164→export interface SpecChatTextMessage {
   165→  type: 'text'
   166→  content: string
   167→}
   168→
   169→export interface SpecChatQuestionMessage {
   170→  type: 'question'
   171→  questions: SpecQuestion[]
   172→  tool_id?: string
   173→}
   174→
   175→export interface SpecChatCompleteMessage {
   176→  type: 'spec_complete'
   177→  path: string
   178→}
   179→
   180→export interface SpecChatFileWrittenMessage {
   181→  type: 'file_written'
   182→  path: string
   183→}
   184→
   185→export interface SpecChatSessionCompleteMessage {
   186→  type: 'complete'
   187→}
   188→
   189→export interface SpecChatErrorMessage {
   190→  type: 'error'
   191→  content: string
   192→}
   193→
   194→export interface SpecChatPongMessage {
   195→  type: 'pong'
   196→}
   197→
   198→export interface SpecChatResponseDoneMessage {
   199→  type: 'response_done'
   200→}
   201→
   202→export type SpecChatServerMessage =
   203→  | SpecChatTextMessage
   204→  | SpecChatQuestionMessage
   205→  | SpecChatCompleteMessage
   206→  | SpecChatFileWrittenMessage
   207→  | SpecChatSessionCompleteMessage
   208→  | SpecChatErrorMessage
   209→  | SpecChatPongMessage
   210→  | SpecChatResponseDoneMessage
   211→
   212→// Image attachment for chat messages
   213→export interface ImageAttachment {
   214→  id: string
   215→  filename: string
   216→  mimeType: 'image/jpeg' | 'image/png'
   217→  base64Data: string    // Raw base64 (without data: prefix)
   218→  previewUrl: string    // data: URL for display
   219→  size: number          // File size in bytes
   220→}
   221→
   222→// UI chat message for display
   223→export interface ChatMessage {
   224→  id: string
   225→  role: 'user' | 'assistant' | 'system'
   226→  content: string
   227→  attachments?: ImageAttachment[]
   228→  timestamp: Date
   229→  questions?: SpecQuestion[]
   230→  isStreaming?: boolean
   231→}
   232→
   233→// ============================================================================
   234→// Assistant Chat Types
   235→// ============================================================================
   236→
   237→export interface AssistantConversation {
   238→  id: number
   239→  project_name: string
   240→  title: string | null
   241→  created_at: string | null
   242→  updated_at: string | null
   243→  message_count: number
   244→}
   245→
   246→export interface AssistantMessage {
   247→  id: number
   248→  role: 'user' | 'assistant' | 'system'
   249→  content: string
   250→  timestamp: string | null
   251→}
   252→
   253→export interface AssistantConversationDetail {
   254→  id: number
   255→  project_name: string
   256→  title: string | null
   257→  created_at: string | null
   258→  updated_at: string | null
   259→  messages: AssistantMessage[]
   260→}
   261→
   262→export interface AssistantChatTextMessage {
   263→  type: 'text'
   264→  content: string
   265→}
   266→
   267→export interface AssistantChatToolCallMessage {
   268→  type: 'tool_call'
   269→  tool: string
   270→  input: Record<string, unknown>
   271→}
   272→
   273→export interface AssistantChatResponseDoneMessage {
   274→  type: 'response_done'
   275→}
   276→
   277→export interface AssistantChatErrorMessage {
   278→  type: 'error'
   279→  content: string
   280→}
   281→
   282→export interface AssistantChatConversationCreatedMessage {
   283→  type: 'conversation_created'
   284→  conversation_id: number
   285→}
   286→
   287→export interface AssistantChatPongMessage {
   288→  type: 'pong'
   289→}
   290→
   291→export type AssistantChatServerMessage =
   292→  | AssistantChatTextMessage
   293→  | AssistantChatToolCallMessage
   294→  | AssistantChatResponseDoneMessage
   295→  | AssistantChatErrorMessage
   296→  | AssistantChatConversationCreatedMessage
   297→  | AssistantChatPongMessage
   298→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

/**
 * TypeScript types for the Autonomous Coding UI
 */

// Project types
export interface ProjectStats {
  passing: number
  in_progress: number
  total: number
  percentage: number
}

export interface ProjectSummary {
  name: string
  path: string
  has_spec: boolean
  stats: ProjectStats
}

export interface ProjectDetail extends ProjectSummary {
  prompts_dir: string
}

// Filesystem types
export interface DriveInfo {
  letter: string
  label: string
  available?: boolean
}

export interface DirectoryEntry {
  name: string
  path: string
  is_directory: boolean
  has_children: boolean
}

export interface DirectoryListResponse {
  current_path: string
  parent_path: string | null
  entries: DirectoryEntry[]
  drives: DriveInfo[] | null
}

export interface PathValidationResponse {
  valid: boolean
  exists: boolean
  is_directory: boolean
  can_write: boolean
  message: string
}

export interface ProjectPrompts {
  app_spec: string
  initializer_prompt: string
  coding_prompt: string
}

// Feature types
export interface Feature {
  id: number
  priority: number
  category: string
  name: string
  description: string
  steps: string[]
  passes: boolean
  in_progress: boolean
}

export interface FeatureListResponse {
  pending: Feature[]
  in_progress: Feature[]
  done: Feature[]
}

export interface FeatureCreate {
  category: string
  name: string
  description: string
  steps: string[]
  priority?: number
}

// Agent types
export type AgentStatus = 'stopped' | 'running' | 'paused' | 'crashed'

export interface AgentStatusResponse {
  status: AgentStatus
  pid: number | null
  started_at: string | null
  yolo_mode: boolean
}

export interface AgentActionResponse {
  success: boolean
  status: AgentStatus
  message: string
}

// Setup types
export interface SetupStatus {
  claude_cli: boolean
  credentials: boolean
  node: boolean
  npm: boolean
}

// WebSocket message types
export type WSMessageType = 'progress' | 'feature_update' | 'log' | 'agent_status' | 'pong'

export interface WSProgressMessage {
  type: 'progress'
  passing: number
  in_progress: number
  total: number
  percentage: number
}

export interface WSFeatureUpdateMessage {
  type: 'feature_update'
  feature_id: number
  passes: boolean
}

export interface WSLogMessage {
  type: 'log'
  line: string
  timestamp: string
}

export interface WSAgentStatusMessage {
  type: 'agent_status'
  status: AgentStatus
}

export interface WSPongMessage {
  type: 'pong'
}

export type WSMessage =
  | WSProgressMessage
  | WSFeatureUpdateMessage
  | WSLogMessage
  | WSAgentStatusMessage
  | WSPongMessage

// ============================================================================
// Spec Chat Types
// ============================================================================

export interface SpecQuestionOption {
  label: string
  description: string
}

export interface SpecQuestion {
  question: string
  header: string
  options: SpecQuestionOption[]
  multiSelect: boolean
}

export interface SpecChatTextMessage {
  type: 'text'
  content: string
}

export interface SpecChatQuestionMessage {
  type: 'question'
  questions: SpecQuestion[]
  tool_id?: string
}

export interface SpecChatCompleteMessage {
  type: 'spec_complete'
  path: string
}

export interface SpecChatFileWrittenMessage {
  type: 'file_written'
  path: string
}

export interface SpecChatSessionCompleteMessage {
  type: 'complete'
}

export interface SpecChatErrorMessage {
  type: 'error'
  content: string
}

export interface SpecChatPongMessage {
  type: 'pong'
}

export interface SpecChatResponseDoneMessage {
  type: 'response_done'
}

export type SpecChatServerMessage =
  | SpecChatTextMessage
  | SpecChatQuestionMessage
  | SpecChatCompleteMessage
  | SpecChatFileWrittenMessage
  | SpecChatSessionCompleteMessage
  | SpecChatErrorMessage
  | SpecChatPongMessage
  | SpecChatResponseDoneMessage

// Image attachment for chat messages
export interface ImageAttachment {
  id: string
  filename: string
  mimeType: 'image/jpeg' | 'image/png'
  base64Data: string    // Raw base64 (without data: prefix)
  previewUrl: string    // data: URL for display
  size: number          // File size in bytes
}

// UI chat message for display
export interface ChatMessage {
  id: string
  role: 'user' | 'assistant' | 'system'
  content: string
  attachments?: ImageAttachment[]
  timestamp: Date
  questions?: SpecQuestion[]
  isStreaming?: boolean
}

// ============================================================================
// Assistant Chat Types
// ============================================================================

export interface AssistantConversation {
  id: number
  project_name: string
  title: string | null
  created_at: string | null
  updated_at: string | null
  message_count: number
}

export interface AssistantMessage {
  id: number
  role: 'user' | 'assistant' | 'system'
  content: string
  timestamp: string | null
}

export interface AssistantConversationDetail {
  id: number
  project_name: string
  title: string | null
  created_at: string | null
  updated_at: string | null
  messages: AssistantMessage[]
}

export interface AssistantChatTextMessage {
  type: 'text'
  content: string
}

export interface AssistantChatToolCallMessage {
  type: 'tool_call'
  tool: string
  input: Record<string, unknown>
}

export interface AssistantChatResponseDoneMessage {
  type: 'response_done'
}

export interface AssistantChatErrorMessage {
  type: 'error'
  content: string
}

export interface AssistantChatConversationCreatedMessage {
  type: 'conversation_created'
  conversation_id: number
}

export interface AssistantChatPongMessage {
  type: 'pong'
}

export type AssistantChatServerMessage =
  | AssistantChatTextMessage
  | AssistantChatToolCallMessage
  | AssistantChatResponseDoneMessage
  | AssistantChatErrorMessage
  | AssistantChatConversationCreatedMessage
  | AssistantChatPongMessage


     1→@import "tailwindcss";
     2→
     3→/* ============================================================================
     4→   Neobrutalism Design System
     5→   ============================================================================ */
     6→
     7→@theme {
     8→  /* Colors */
     9→  --color-neo-bg: #fffef5;
    10→  --color-neo-card: #ffffff;
    11→  --color-neo-pending: #ffd60a;
    12→  --color-neo-progress: #00b4d8;
    13→  --color-neo-done: #70e000;
    14→  --color-neo-accent: #ff006e;
    15→  --color-neo-danger: #ff5400;
    16→  --color-neo-border: #1a1a1a;
    17→  --color-neo-text: #1a1a1a;
    18→  --color-neo-text-secondary: #4a4a4a;
    19→
    20→  /* Fonts */
    21→  --font-neo-display: 'Space Grotesk', sans-serif;
    22→  --font-neo-sans: 'DM Sans', sans-serif;
    23→  --font-neo-mono: 'JetBrains Mono', monospace;
    24→
    25→  /* Shadows */
    26→  --shadow-neo-sm: 2px 2px 0px rgba(0, 0, 0, 1);
    27→  --shadow-neo-md: 4px 4px 0px rgba(0, 0, 0, 1);
    28→  --shadow-neo-lg: 6px 6px 0px rgba(0, 0, 0, 1);
    29→  --shadow-neo-xl: 8px 8px 0px rgba(0, 0, 0, 1);
    30→
    31→  /* Transitions */
    32→  --transition-neo-fast: 0.15s cubic-bezier(0.34, 1.56, 0.64, 1);
    33→  --transition-neo-normal: 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
    34→}
    35→
    36→/* ============================================================================
    37→   Base Layer - Foundation styles
    38→   ============================================================================ */
    39→
    40→@layer base {
    41→  /* Ensure CSS variables are available globally */
    42→  :root {
    43→    --color-neo-bg: #fffef5;
    44→    --color-neo-card: #ffffff;
    45→    --color-neo-pending: #ffd60a;
    46→    --color-neo-progress: #00b4d8;
    47→    --color-neo-done: #70e000;
    48→    --color-neo-accent: #ff006e;
    49→    --color-neo-danger: #ff5400;
    50→    --color-neo-border: #1a1a1a;
    51→    --color-neo-text: #1a1a1a;
    52→    --color-neo-text-secondary: #4a4a4a;
    53→    --font-neo-display: 'Space Grotesk', sans-serif;
    54→    --font-neo-sans: 'DM Sans', sans-serif;
    55→    --font-neo-mono: 'JetBrains Mono', monospace;
    56→    --shadow-neo-sm: 2px 2px 0px rgba(0, 0, 0, 1);
    57→    --shadow-neo-md: 4px 4px 0px rgba(0, 0, 0, 1);
    58→    --shadow-neo-lg: 6px 6px 0px rgba(0, 0, 0, 1);
    59→    --shadow-neo-xl: 8px 8px 0px rgba(0, 0, 0, 1);
    60→  }
    61→
    62→  body {
    63→    font-family: var(--font-neo-sans);
    64→    background-color: var(--color-neo-bg);
    65→    color: var(--color-neo-text);
    66→  }
    67→
    68→  /* Force form elements to inherit colors properly */
    69→  button,
    70→  input,
    71→  textarea,
    72→  select {
    73→    color: inherit;
    74→    font-family: inherit;
    75→  }
    76→}
    77→
    78→/* ============================================================================
    79→   Component Layer - Reusable component styles
    80→   ============================================================================ */
    81→
    82→@layer components {
    83→  /* Cards */
    84→  .neo-card {
    85→    background: var(--color-neo-card);
    86→    border: 3px solid var(--color-neo-border);
    87→    box-shadow: var(--shadow-neo-md);
    88→    transition: transform var(--transition-neo-fast), box-shadow var(--transition-neo-fast);
    89→  }
    90→
    91→  .neo-card:hover {
    92→    transform: translate(-2px, -2px);
    93→    box-shadow: var(--shadow-neo-lg);
    94→  }
    95→
    96→  /* Buttons */
    97→  .neo-btn {
    98→    display: inline-flex;
    99→    align-items: center;
   100→    justify-content: center;
   101→    gap: 0.5rem;
   102→    padding: 0.75rem 1.5rem;
   103→    font-family: var(--font-neo-display);
   104→    font-weight: 700;
   105→    font-size: 0.875rem;
   106→    text-transform: uppercase;
   107→    letter-spacing: 0.025em;
   108→    color: #1a1a1a;
   109→    background-color: #ffffff;
   110→    border: 3px solid #1a1a1a;
   111→    box-shadow: var(--shadow-neo-md);
   112→    transition: transform var(--transition-neo-fast), box-shadow var(--transition-neo-fast);
   113→    cursor: pointer;
   114→  }
   115→
   116→  .neo-btn:hover {
   117→    transform: translate(-2px, -2px);
   118→    box-shadow: var(--shadow-neo-lg);
   119→  }
   120→
   121→  .neo-btn:active {
   122→    transform: translate(2px, 2px);
   123→    box-shadow: var(--shadow-neo-sm);
   124→  }
   125→
   126→  .neo-btn:disabled {
   127→    opacity: 0.5;
   128→    cursor: not-allowed;
   129→    transform: none;
   130→    box-shadow: var(--shadow-neo-sm);
   131→  }
   132→
   133→  .neo-btn-primary {
   134→    background-color: #ff006e;
   135→    color: #ffffff;
   136→  }
   137→
   138→  .neo-btn-success {
   139→    background-color: #70e000;
   140→    color: #1a1a1a;
   141→  }
   142→
   143→  .neo-btn-warning {
   144→    background-color: #ffd60a;
   145→    color: #1a1a1a;
   146→  }
   147→
   148→  .neo-btn-danger {
   149→    background-color: #ff5400;
   150→    color: #ffffff;
   151→  }
   152→
   153→  .neo-btn-ghost {
   154→    background-color: transparent;
   155→    color: #1a1a1a;
   156→    box-shadow: none;
   157→  }
   158→
   159→  .neo-btn-ghost:hover {
   160→    background-color: rgba(0, 0, 0, 0.05);
   161→    color: #1a1a1a;
   162→    box-shadow: none;
   163→    transform: none;
   164→  }
   165→
   166→  /* Inputs */
   167→  .neo-input {
   168→    width: 100%;
   169→    padding: 0.75rem 1rem;
   170→    font-family: var(--font-neo-sans);
   171→    font-size: 1rem;
   172→    color: #1a1a1a;
   173→    background-color: #ffffff;
   174→    border: 3px solid #1a1a1a;
   175→    box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.1);
   176→    transition: transform var(--transition-neo-fast), box-shadow var(--transition-neo-fast);
   177→  }
   178→
   179→  .neo-input::placeholder {
   180→    color: #4a4a4a;
   181→    opacity: 0.7;
   182→  }
   183→
   184→  .neo-input:focus {
   185→    outline: none;
   186→    transform: translate(-1px, -1px);
   187→    box-shadow: var(--shadow-neo-md);
   188→    border-color: #ff006e;
   189→  }
   190→
   191→  /* Badge */
   192→  .neo-badge {
   193→    display: inline-flex;
   194→    align-items: center;
   195→    padding: 0.25rem 0.75rem;
   196→    font-family: var(--font-neo-display);
   197→    font-size: 0.75rem;
   198→    font-weight: 700;
   199→    text-transform: uppercase;
   200→    color: #1a1a1a;
   201→    border: 2px solid #1a1a1a;
   202→  }
   203→
   204→  /* Progress Bar */
   205→  .neo-progress {
   206→    width: 100%;
   207→    height: 2rem;
   208→    background-color: #ffffff;
   209→    border: 3px solid #1a1a1a;
   210→    box-shadow: var(--shadow-neo-sm);
   211→    overflow: hidden;
   212→  }
   213→
   214→  .neo-progress-fill {
   215→    height: 100%;
   216→    background-color: #70e000;
   217→    transition: width 0.5s ease-out;
   218→  }
   219→
   220→  /* Modal */
   221→  .neo-modal-backdrop {
   222→    position: fixed;
   223→    inset: 0;
   224→    background: rgba(0, 0, 0, 0.4);
   225→    backdrop-filter: blur(2px);
   226→    display: flex;
   227→    align-items: center;
   228→    justify-content: center;
   229→    z-index: 50;
   230→  }
   231→
   232→  .neo-modal {
   233→    background-color: #ffffff;
   234→    border: 4px solid #1a1a1a;
   235→    box-shadow: var(--shadow-neo-xl);
   236→    animation: popIn 0.3s var(--transition-neo-fast);
   237→    max-width: 90vw;
   238→    max-height: 90vh;
   239→    overflow: auto;
   240→  }
   241→
   242→  /* Dropdown */
   243→  .neo-dropdown {
   244→    background-color: #ffffff;
   245→    border: 3px solid #1a1a1a;
   246→    box-shadow: var(--shadow-neo-lg);
   247→  }
   248→
   249→  .neo-dropdown-item {
   250→    display: block;
   251→    width: 100%;
   252→    padding: 0.75rem 1rem;
   253→    cursor: pointer;
   254→    color: #1a1a1a;
   255→    background-color: transparent;
   256→    text-align: left;
   257→    border: none;
   258→    font-size: 0.875rem;
   259→    transition: background-color var(--transition-neo-fast);
   260→  }
   261→
   262→  .neo-dropdown-item:hover {
   263→    background-color: #ffd60a;
   264→    color: #1a1a1a;
   265→  }
   266→
   267→  /* Tooltip */
   268→  .neo-tooltip {
   269→    background-color: #1a1a1a;
   270→    color: #ffffff;
   271→    padding: 0.5rem 0.75rem;
   272→    font-size: 0.75rem;
   273→    font-weight: 700;
   274→    text-transform: uppercase;
   275→    border: 2px solid #1a1a1a;
   276→    box-shadow: var(--shadow-neo-sm);
   277→  }
   278→
   279→  /* Empty State */
   280→  .neo-empty-state {
   281→    background-color: var(--color-neo-bg);
   282→    border: 4px dashed #1a1a1a;
   283→    padding: 2rem;
   284→    text-align: center;
   285→  }
   286→}
   287→
   288→/* ============================================================================
   289→   Animations
   290→   ============================================================================ */
   291→
   292→@keyframes popIn {
   293→  from {
   294→    opacity: 0;
   295→    transform: scale(0.9);
   296→  }
   297→  to {
   298→    opacity: 1;
   299→    transform: scale(1);
   300→  }
   301→}
   302→
   303→@keyframes slideIn {
   304→  from {
   305→    opacity: 0;
   306→    transform: translateX(-20px);
   307→  }
   308→  to {
   309→    opacity: 1;
   310→    transform: translateX(0);
   311→  }
   312→}
   313→
   314→@keyframes neoPulse {
   315→  0%, 100% {
   316→    box-shadow: 6px 6px 0px rgba(0, 180, 216, 0.3);
   317→  }
   318→  50% {
   319→    box-shadow: 6px 6px 0px rgba(0, 180, 216, 0.8);
   320→  }
   321→}
   322→
   323→@keyframes completePop {
   324→  0% {
   325→    transform: scale(0.8);
   326→  }
   327→  60% {
   328→    transform: scale(1.15);
   329→  }
   330→  100% {
   331→    transform: scale(1);
   332→  }
   333→}
   334→
   335→@keyframes thoughtFade {
   336→  0% {
   337→    opacity: 0;
   338→    transform: translateY(-4px);
   339→    filter: blur(2px);
   340→  }
   341→  100% {
   342→    opacity: 1;
   343→    transform: translateY(0);
   344→    filter: blur(0);
   345→  }
   346→}
   347→
   348→@keyframes shimmer {
   349→  0% {
   350→    background-position: 200% center;
   351→  }
   352→  100% {
   353→    background-position: -200% center;
   354→  }
   355→}
   356→
   357→/* ============================================================================
   358→   Utilities Layer
   359→   ============================================================================ */
   360→
   361→@layer utilities {
   362→  .animate-slide-in {
   363→    animation: slideIn 0.3s ease-out;
   364→  }
   365→
   366→  .animate-pop-in {
   367→    animation: popIn 0.3s var(--transition-neo-fast);
   368→  }
   369→
   370→  .animate-pulse-neo {
   371→    animation: neoPulse 2s infinite;
   372→  }
   373→
   374→  .animate-complete {
   375→    animation: completePop 0.5s var(--transition-neo-fast);
   376→  }
   377→
   378→  .animate-thought-fade {
   379→    animation: thoughtFade 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) backwards;
   380→  }
   381→
   382→  .animate-shimmer {
   383→    background: linear-gradient(
   384→      90deg,
   385→      var(--color-neo-text) 0%,
   386→      var(--color-neo-text) 40%,
   387→      var(--color-neo-progress) 50%,
   388→      var(--color-neo-text) 60%,
   389→      var(--color-neo-text) 100%
   390→    );
   391→    background-size: 200% 100%;
   392→    background-clip: text;
   393→    -webkit-background-clip: text;
   394→    color: transparent;
   395→    animation: shimmer 2s ease-in-out infinite;
   396→  }
   397→
   398→  .font-display {
   399→    font-family: var(--font-neo-display);
   400→  }
   401→
   402→  .font-sans {
   403→    font-family: var(--font-neo-sans);
   404→  }
   405→
   406→  .font-mono {
   407→    font-family: var(--font-neo-mono);
   408→  }
   409→}
   410→
   411→/* ============================================================================
   412→   Scrollbar Styling (outside layers for broad compatibility)
   413→   ============================================================================ */
   414→
   415→::-webkit-scrollbar {
   416→  width: 12px;
   417→  height: 12px;
   418→}
   419→
   420→::-webkit-scrollbar-track {
   421→  background: #fffef5;
   422→  border: 2px solid #1a1a1a;
   423→}
   424→
   425→::-webkit-scrollbar-thumb {
   426→  background: #1a1a1a;
   427→  border: 2px solid #1a1a1a;
   428→}
   429→
   430→::-webkit-scrollbar-thumb:hover {
   431→  background: #4a4a4a;
   432→}
   433→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>

@import "tailwindcss";

/* ============================================================================
   Neobrutalism Design System
   ============================================================================ */

@theme {
  /* Colors */
  --color-neo-bg: #fffef5;
  --color-neo-card: #ffffff;
  --color-neo-pending: #ffd60a;
  --color-neo-progress: #00b4d8;
  --color-neo-done: #70e000;
  --color-neo-accent: #ff006e;
  --color-neo-danger: #ff5400;
  --color-neo-border: #1a1a1a;
  --color-neo-text: #1a1a1a;
  --color-neo-text-secondary: #4a4a4a;

  /* Fonts */
  --font-neo-display: 'Space Grotesk', sans-serif;
  --font-neo-sans: 'DM Sans', sans-serif;
  --font-neo-mono: 'JetBrains Mono', monospace;

  /* Shadows */
  --shadow-neo-sm: 2px 2px 0px rgba(0, 0, 0, 1);
  --shadow-neo-md: 4px 4px 0px rgba(0, 0, 0, 1);
  --shadow-neo-lg: 6px 6px 0px rgba(0, 0, 0, 1);
  --shadow-neo-xl: 8px 8px 0px rgba(0, 0, 0, 1);

  /* Transitions */
  --transition-neo-fast: 0.15s cubic-bezier(0.34, 1.56, 0.64, 1);
  --transition-neo-normal: 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}

/* ============================================================================
   Base Layer - Foundation styles
   ============================================================================ */

@layer base {
  /* Ensure CSS variables are available globally */
  :root {
    --color-neo-bg: #fffef5;
    --color-neo-card: #ffffff;
    --color-neo-pending: #ffd60a;
    --color-neo-progress: #00b4d8;
    --color-neo-done: #70e000;
    --color-neo-accent: #ff006e;
    --color-neo-danger: #ff5400;
    --color-neo-border: #1a1a1a;
    --color-neo-text: #1a1a1a;
    --color-neo-text-secondary: #4a4a4a;
    --font-neo-display: 'Space Grotesk', sans-serif;
    --font-neo-sans: 'DM Sans', sans-serif;
    --font-neo-mono: 'JetBrains Mono', monospace;
    --shadow-neo-sm: 2px 2px 0px rgba(0, 0, 0, 1);
    --shadow-neo-md: 4px 4px 0px rgba(0, 0, 0, 1);
    --shadow-neo-lg: 6px 6px 0px rgba(0, 0, 0, 1);
    --shadow-neo-xl: 8px 8px 0px rgba(0, 0, 0, 1);
  }

  body {
    font-family: var(--font-neo-sans);
    background-color: var(--color-neo-bg);
    color: var(--color-neo-text);
  }

  /* Force form elements to inherit colors properly */
  button,
  input,
  textarea,
  select {
    color: inherit;
    font-family: inherit;
  }
}

/* ============================================================================
   Component Layer - Reusable component styles
   ============================================================================ */

@layer components {
  /* Cards */
  .neo-card {
    background: var(--color-neo-card);
    border: 3px solid var(--color-neo-border);
    box-shadow: var(--shadow-neo-md);
    transition: transform var(--transition-neo-fast), box-shadow var(--transition-neo-fast);
  }

  .neo-card:hover {
    transform: translate(-2px, -2px);
    box-shadow: var(--shadow-neo-lg);
  }

  /* Buttons */
  .neo-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 0.5rem;
    padding: 0.75rem 1.5rem;
    font-family: var(--font-neo-display);
    font-weight: 700;
    font-size: 0.875rem;
    text-transform: uppercase;
    letter-spacing: 0.025em;
    color: #1a1a1a;
    background-color: #ffffff;
    border: 3px solid #1a1a1a;
    box-shadow: var(--shadow-neo-md);
    transition: transform var(--transition-neo-fast), box-shadow var(--transition-neo-fast);
    cursor: pointer;
  }

  .neo-btn:hover {
    transform: translate(-2px, -2px);
    box-shadow: var(--shadow-neo-lg);
  }

  .neo-btn:active {
    transform: translate(2px, 2px);
    box-shadow: var(--shadow-neo-sm);
  }

  .neo-btn:disabled {
    opacity: 0.5;
    cursor: not-allowed;
    transform: none;
    box-shadow: var(--shadow-neo-sm);
  }

  .neo-btn-primary {
    background-color: #ff006e;
    color: #ffffff;
  }

  .neo-btn-success {
    background-color: #70e000;
    color: #1a1a1a;
  }

  .neo-btn-warning {
    background-color: #ffd60a;
    color: #1a1a1a;
  }

  .neo-btn-danger {
    background-color: #ff5400;
    color: #ffffff;
  }

  .neo-btn-ghost {
    background-color: transparent;
    color: #1a1a1a;
    box-shadow: none;
  }

  .neo-btn-ghost:hover {
    background-color: rgba(0, 0, 0, 0.05);
    color: #1a1a1a;
    box-shadow: none;
    transform: none;
  }

  /* Inputs */
  .neo-input {
    width: 100%;
    padding: 0.75rem 1rem;
    font-family: var(--font-neo-sans);
    font-size: 1rem;
    color: #1a1a1a;
    background-color: #ffffff;
    border: 3px solid #1a1a1a;
    box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.1);
    transition: transform var(--transition-neo-fast), box-shadow var(--transition-neo-fast);
  }

  .neo-input::placeholder {
    color: #4a4a4a;
    opacity: 0.7;
  }

  .neo-input:focus {
    outline: none;
    transform: translate(-1px, -1px);
    box-shadow: var(--shadow-neo-md);
    border-color: #ff006e;
  }

  /* Badge */
  .neo-badge {
    display: inline-flex;
    align-items: center;
    padding: 0.25rem 0.75rem;
    font-family: var(--font-neo-display);
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    color: #1a1a1a;
    border: 2px solid #1a1a1a;
  }

  /* Progress Bar */
  .neo-progress {
    width: 100%;
    height: 2rem;
    background-color: #ffffff;
    border: 3px solid #1a1a1a;
    box-shadow: var(--shadow-neo-sm);
    overflow: hidden;
  }

  .neo-progress-fill {
    height: 100%;
    background-color: #70e000;
    transition: width 0.5s ease-out;
  }

  /* Modal */
  .neo-modal-backdrop {
    position: fixed;
    inset: 0;
    background: rgba(0, 0, 0, 0.4);
    backdrop-filter: blur(2px);
    display: flex;
    align-items: center;
    justify-content: center;
    z-index: 50;
  }

  .neo-modal {
    background-color: #ffffff;
    border: 4px solid #1a1a1a;
    box-shadow: var(--shadow-neo-xl);
    animation: popIn 0.3s var(--transition-neo-fast);
    max-width: 90vw;
    max-height: 90vh;
    overflow: auto;
  }

  /* Dropdown */
  .neo-dropdown {
    background-color: #ffffff;
    border: 3px solid #1a1a1a;
    box-shadow: var(--shadow-neo-lg);
  }

  .neo-dropdown-item {
    display: block;
    width: 100%;
    padding: 0.75rem 1rem;
    cursor: pointer;
    color: #1a1a1a;
    background-color: transparent;
    text-align: left;
    border: none;
    font-size: 0.875rem;
    transition: background-color var(--transition-neo-fast);
  }

  .neo-dropdown-item:hover {
    background-color: #ffd60a;
    color: #1a1a1a;
  }

  /* Tooltip */
  .neo-tooltip {
    background-color: #1a1a1a;
    color: #ffffff;
    padding: 0.5rem 0.75rem;
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    border: 2px solid #1a1a1a;
    box-shadow: var(--shadow-neo-sm);
  }

  /* Empty State */
  .neo-empty-state {
    background-color: var(--color-neo-bg);
    border: 4px dashed #1a1a1a;
    padding: 2rem;
    text-align: center;
  }
}

/* ============================================================================
   Animations
   ============================================================================ */

@keyframes popIn {
  from {
    opacity: 0;
    transform: scale(0.9);
  }
  to {
    opacity: 1;
    transform: scale(1);
  }
}

@keyframes slideIn {
  from {
    opacity: 0;
    transform: translateX(-20px);
  }
  to {
    opacity: 1;
    transform: translateX(0);
  }
}

@keyframes neoPulse {
  0%, 100% {
    box-shadow: 6px 6px 0px rgba(0, 180, 216, 0.3);
  }
  50% {
    box-shadow: 6px 6px 0px rgba(0, 180, 216, 0.8);
  }
}

@keyframes completePop {
  0% {
    transform: scale(0.8);
  }
  60% {
    transform: scale(1.15);
  }
  100% {
    transform: scale(1);
  }
}

@keyframes thoughtFade {
  0% {
    opacity: 0;
    transform: translateY(-4px);
    filter: blur(2px);
  }
  100% {
    opacity: 1;
    transform: translateY(0);
    filter: blur(0);
  }
}

@keyframes shimmer {
  0% {
    background-position: 200% center;
  }
  100% {
    background-position: -200% center;
  }
}

/* ============================================================================
   Utilities Layer
   ============================================================================ */

@layer utilities {
  .animate-slide-in {
    animation: slideIn 0.3s ease-out;
  }

  .animate-pop-in {
    animation: popIn 0.3s var(--transition-neo-fast);
  }

  .animate-pulse-neo {
    animation: neoPulse 2s infinite;
  }

  .animate-complete {
    animation: completePop 0.5s var(--transition-neo-fast);
  }

  .animate-thought-fade {
    animation: thoughtFade 0.4s cubic-bezier(0.34, 1.56, 0.64, 1) backwards;
  }

  .animate-shimmer {
    background: linear-gradient(
      90deg,
      var(--color-neo-text) 0%,
      var(--color-neo-text) 40%,
      var(--color-neo-progress) 50%,
      var(--color-neo-text) 60%,
      var(--color-neo-text) 100%
    );
    background-size: 200% 100%;
    background-clip: text;
    -webkit-background-clip: text;
    color: transparent;
    animation: shimmer 2s ease-in-out infinite;
  }

  .font-display {
    font-family: var(--font-neo-display);
  }

  .font-sans {
    font-family: var(--font-neo-sans);
  }

  .font-mono {
    font-family: var(--font-neo-mono);
  }
}

/* ============================================================================
   Scrollbar Styling (outside layers for broad compatibility)
   ============================================================================ */

::-webkit-scrollbar {
  width: 12px;
  height: 12px;
}

::-webkit-scrollbar-track {
  background: #fffef5;
  border: 2px solid #1a1a1a;
}

::-webkit-scrollbar-thumb {
  background: #1a1a1a;
  border: 2px solid #1a1a1a;
}

::-webkit-scrollbar-thumb:hover {
  background: #4a4a4a;
}


Now I have a complete understanding of the design system and patterns. I need to create:

1. `ui/src/hooks/useGit.ts` - Hook for git WebSocket and REST API
2. `ui/src/components/GitPanel.tsx` - Panel component for git status

Let me create these files.


Now I have a good understanding of the patterns. Let me create the git hook and panel component.


Write

/**
 * Hook for managing git status and operations
 *
 * Provides real-time git status updates via WebSocket and
 * REST API methods for git operations.
 */

import { useState, useCallback, useRef, useEffect } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'

type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error'

export interface GitStatus {
  branch: string
  ahead: number
  behind: number
  staged: string[]
  unstaged: string[]
  untracked: string[]
  has_conflicts: boolean
  is_clean: boolean
  remote?: string
  last_commit_hash?: string
  last_commit_message?: string
  last_commit_time?: string
}

export interface GitCommit {
  hash: string
  short_hash: string
  message: string
  author: string
  date: string
}

export interface GitResult {
  success: boolean
  output: string
  error?: string
  commit_hash?: string
}

export interface UseGitOptions {
  projectName: string
  autoConnect?: boolean
  onError?: (error: string) => void
  onStatusUpdate?: (status: GitStatus) => void
}

export interface UseGitReturn {
  status: GitStatus | null
  commits: GitCommit[]
  isLoading: boolean
  connectionStatus: ConnectionStatus
  connect: () => void
  disconnect: () => void
  refresh: () => void
  stageFiles: (files?: string[]) => Promise<GitResult>
  commit: (message: string, files?: string[]) => Promise<GitResult>
  push: (options?: { remote?: string; branch?: string; force?: boolean; setUpstream?: boolean }) => Promise<GitResult>
  pull: (options?: { remote?: string; branch?: string; rebase?: boolean }) => Promise<GitResult>
  fetch: (remote?: string) => Promise<GitResult>
  checkout: (branch: string, create?: boolean) => Promise<GitResult>
  getDiff: (staged?: boolean, file?: string) => Promise<string>
}

const API_BASE = '/api/git/projects'

async function fetchGitStatus(projectName: string): Promise<GitStatus> {
  const response = await fetch(`${API_BASE}/${encodeURIComponent(projectName)}/status`)
  if (!response.ok) {
    throw new Error(`Failed to fetch git status: ${response.statusText}`)
  }
  return response.json()
}

async function fetchGitLog(projectName: string, count: number = 10): Promise<GitCommit[]> {
  const response = await fetch(`${API_BASE}/${encodeURIComponent(projectName)}/log?count=${count}`)
  if (!response.ok) {
    throw new Error(`Failed to fetch git log: ${response.statusText}`)
  }
  return response.json()
}

export function useGit({
  projectName,
  autoConnect = false,
  onError,
  onStatusUpdate,
}: UseGitOptions): UseGitReturn {
  const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('disconnected')
  const [wsStatus, setWsStatus] = useState<GitStatus | null>(null)

  const wsRef = useRef<WebSocket | null>(null)
  const pingIntervalRef = useRef<number | null>(null)
  const reconnectTimeoutRef = useRef<number | null>(null)
  const reconnectAttempts = useRef(0)
  const maxReconnectAttempts = 3

  const queryClient = useQueryClient()

  // REST API queries
  const {
    data: restStatus,
    isLoading: statusLoading,
    refetch: refetchStatus,
  } = useQuery({
    queryKey: ['git-status', projectName],
    queryFn: () => fetchGitStatus(projectName),
    enabled: !!projectName,
    refetchInterval: false, // WebSocket handles real-time updates
    retry: 1,
  })

  const {
    data: commits = [],
    isLoading: commitsLoading,
    refetch: refetchCommits,
  } = useQuery({
    queryKey: ['git-log', projectName],
    queryFn: () => fetchGitLog(projectName),
    enabled: !!projectName,
    retry: 1,
  })

  // Prefer WebSocket status if connected, otherwise use REST
  const status = wsStatus || restStatus || null

  // Clean up on unmount
  useEffect(() => {
    return () => {
      if (pingIntervalRef.current) {
        clearInterval(pingIntervalRef.current)
      }
      if (reconnectTimeoutRef.current) {
        clearTimeout(reconnectTimeoutRef.current)
      }
      if (wsRef.current) {
        wsRef.current.close()
      }
    }
  }, [])

  // Auto-connect if enabled
  useEffect(() => {
    if (autoConnect && projectName) {
      connect()
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [autoConnect, projectName])

  const connect = useCallback(() => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      return
    }

    setConnectionStatus('connecting')

    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
    const host = window.location.host
    const wsUrl = `${protocol}//${host}/api/git/projects/${encodeURIComponent(projectName)}/ws`

    const ws = new WebSocket(wsUrl)
    wsRef.current = ws

    ws.onopen = () => {
      setConnectionStatus('connected')
      reconnectAttempts.current = 0

      // Subscribe to status updates
      ws.send(JSON.stringify({ type: 'subscribe' }))

      // Start ping interval
      pingIntervalRef.current = window.setInterval(() => {
        if (ws.readyState === WebSocket.OPEN) {
          ws.send(JSON.stringify({ type: 'ping' }))
        }
      }, 30000)
    }

    ws.onclose = () => {
      setConnectionStatus('disconnected')
      if (pingIntervalRef.current) {
        clearInterval(pingIntervalRef.current)
        pingIntervalRef.current = null
      }

      // Attempt reconnection
      if (reconnectAttempts.current < maxReconnectAttempts) {
        reconnectAttempts.current++
        const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 10000)
        reconnectTimeoutRef.current = window.setTimeout(connect, delay)
      }
    }

    ws.onerror = () => {
      setConnectionStatus('error')
      onError?.('WebSocket connection error')
    }

    ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data)

        switch (data.type) {
          case 'status': {
            const newStatus = data.data as GitStatus
            setWsStatus(newStatus)
            onStatusUpdate?.(newStatus)
            break
          }

          case 'error': {
            onError?.(data.content)
            break
          }

          case 'pong': {
            // Ping response - ignore
            break
          }

          default: {
            console.warn('Unknown git message type:', data.type)
          }
        }
      } catch (e) {
        console.error('Failed to parse git message:', e)
      }
    }
  }, [projectName, onError, onStatusUpdate])

  const disconnect = useCallback(() => {
    reconnectAttempts.current = maxReconnectAttempts // Prevent auto-reconnect
    if (reconnectTimeoutRef.current) {
      clearTimeout(reconnectTimeoutRef.current)
      reconnectTimeoutRef.current = null
    }
    if (pingIntervalRef.current) {
      clearInterval(pingIntervalRef.current)
      pingIntervalRef.current = null
    }
    if (wsRef.current) {
      // Unsubscribe before closing
      if (wsRef.current.readyState === WebSocket.OPEN) {
        wsRef.current.send(JSON.stringify({ type: 'unsubscribe' }))
      }
      wsRef.current.close()
      wsRef.current = null
    }
    setConnectionStatus('disconnected')
    setWsStatus(null)
  }, [])

  const refresh = useCallback(() => {
    // Refresh via WebSocket if connected
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      wsRef.current.send(JSON.stringify({ type: 'refresh' }))
    } else {
      // Fall back to REST API
      refetchStatus()
      refetchCommits()
    }
  }, [refetchStatus, refetchCommits])

  // Mutation helpers
  const stageFilesMutation = useMutation({
    mutationFn: async (files?: string[]): Promise<GitResult> => {
      const response = await fetch(`${API_BASE}/${encodeURIComponent(projectName)}/add`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(files || null),
      })
      if (!response.ok) {
        throw new Error(`Stage failed: ${response.statusText}`)
      }
      return response.json()
    },
    onSuccess: () => {
      refresh()
    },
    onError: (error: Error) => {
      onError?.(error.message)
    },
  })

  const commitMutation = useMutation({
    mutationFn: async ({ message, files }: { message: string; files?: string[] }): Promise<GitResult> => {
      const response = await fetch(`${API_BASE}/${encodeURIComponent(projectName)}/commit`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message, files }),
      })
      if (!response.ok) {
        throw new Error(`Commit failed: ${response.statusText}`)
      }
      return response.json()
    },
    onSuccess: () => {
      refresh()
      queryClient.invalidateQueries({ queryKey: ['git-log', projectName] })
    },
    onError: (error: Error) => {
      onError?.(error.message)
    },
  })

  const pushMutation = useMutation({
    mutationFn: async (options?: { remote?: string; branch?: string; force?: boolean; setUpstream?: boolean }): Promise<GitResult> => {
      const response = await fetch(`${API_BASE}/${encodeURIComponent(projectName)}/push`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(options || {}),
      })
      if (!response.ok) {
        throw new Error(`Push failed: ${response.statusText}`)
      }
      return response.json()
    },
    onSuccess: () => {
      refresh()
    },
    onError: (error: Error) => {
      onError?.(error.message)
    },
  })

  const pullMutation = useMutation({
    mutationFn: async (options?: { remote?: string; branch?: string; rebase?: boolean }): Promise<GitResult> => {
      const response = await fetch(`${API_BASE}/${encodeURIComponent(projectName)}/pull`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(options || {}),
      })
      if (!response.ok) {
        throw new Error(`Pull failed: ${response.statusText}`)
      }
      return response.json()
    },
    onSuccess: () => {
      refresh()
      queryClient.invalidateQueries({ queryKey: ['git-log', projectName] })
    },
    onError: (error: Error) => {
      onError?.(error.message)
    },
  })

  const fetchMutation = useMutation({
    mutationFn: async (remote: string = 'origin'): Promise<GitResult> => {
      const response = await fetch(`${API_BASE}/${encodeURIComponent(projectName)}/fetch?remote=${encodeURIComponent(remote)}`, {
        method: 'POST',
      })
      if (!response.ok) {
        throw new Error(`Fetch failed: ${response.statusText}`)
      }
      return response.json()
    },
    onSuccess: () => {
      refresh()
    },
    onError: (error: Error) => {
      onError?.(error.message)
    },
  })

  const checkoutMutation = useMutation({
    mutationFn: async ({ branch, create }: { branch: string; create?: boolean }): Promise<GitResult> => {
      const response = await fetch(`${API_BASE}/${encodeURIComponent(projectName)}/checkout`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ branch, create: create || false }),
      })
      if (!response.ok) {
        throw new Error(`Checkout failed: ${response.statusText}`)
      }
      return response.json()
    },
    onSuccess: () => {
      refresh()
      queryClient.invalidateQueries({ queryKey: ['git-log', projectName] })
    },
    onError: (error: Error) => {
      onError?.(error.message)
    },
  })

  const getDiff = useCallback(async (staged: boolean = false, file?: string): Promise<string> => {
    const params = new URLSearchParams()
    if (staged) params.set('staged', 'true')
    if (file) params.set('file', file)

    const response = await fetch(`${API_BASE}/${encodeURIComponent(projectName)}/diff?${params}`)
    if (!response.ok) {
      throw new Error(`Failed to get diff: ${response.statusText}`)
    }
    const data = await response.json()
    return data.diff
  }, [projectName])

  return {
    status,
    commits,
    isLoading: statusLoading || commitsLoading,
    connectionStatus,
    connect,
    disconnect,
    refresh,
    stageFiles: (files?: string[]) => stageFilesMutation.mutateAsync(files),
    commit: (message: string, files?: string[]) => commitMutation.mutateAsync({ message, files }),
    push: (options) => pushMutation.mutateAsync(options),
    pull: (options) => pullMutation.mutateAsync(options),
    fetch: (remote) => fetchMutation.mutateAsync(remote),
    checkout: (branch: string, create?: boolean) => checkoutMutation.mutateAsync({ branch, create }),
    getDiff,
  }
}


File created successfully at: C:\Users\MikeSwanson\claude-projects\Autocode-remix\Autocode-fork\autocoder-master\ui\src\hooks\useGit.ts

/**
 * Hook for managing git status and operations
 *
 * Provides real-time git status updates via WebSocket and
 * REST API methods for git operations.
 */

import { useState, useCallback, useRef, useEffect } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'

type ConnectionStatus = 'disconnected' | 'connecting' | 'connected' | 'error'

export interface GitStatus {
  branch: string
  ahead: number
  behind: number
  staged: string[]
  unstaged: string[]
  untracked: string[]
  has_conflicts: boolean
  is_clean: boolean
  remote?: string
  last_commit_hash?: string
  last_commit_message?: string
  last_commit_time?: string
}

export interface GitCommit {
  hash: string
  short_hash: string
  message: string
  author: string
  date: string
}

export interface GitResult {
  success: boolean
  output: string
  error?: string
  commit_hash?: string
}

export interface UseGitOptions {
  projectName: string
  autoConnect?: boolean
  onError?: (error: string) => void
  onStatusUpdate?: (status: GitStatus) => void
}

export interface UseGitReturn {
  status: GitStatus | null
  commits: GitCommit[]
  isLoading: boolean
  connectionStatus: ConnectionStatus
  connect: () => void
  disconnect: () => void
  refresh: () => void
  stageFiles: (files?: string[]) => Promise<GitResult>
  commit: (message: string, files?: string[]) => Promise<GitResult>
  push: (options?: { remote?: string; branch?: string; force?: boolean; setUpstream?: boolean }) => Promise<GitResult>
  pull: (options?: { remote?: string; branch?: string; rebase?: boolean }) => Promise<GitResult>
  fetch: (remote?: string) => Promise<GitResult>
  checkout: (branch: string, create?: boolean) => Promise<GitResult>
  getDiff: (staged?: boolean, file?: string) => Promise<string>
}

const API_BASE = '/api/git/projects'

async function fetchGitStatus(projectName: string): Promise<GitStatus> {
  const response = await fetch(`${API_BASE}/${encodeURIComponent(projectName)}/status`)
  if (!response.ok) {
    throw new Error(`Failed to fetch git status: ${response.statusText}`)
  }
  return response.json()
}

async function fetchGitLog(projectName: string, count: number = 10): Promise<GitCommit[]> {
  const response = await fetch(`${API_BASE}/${encodeURIComponent(projectName)}/log?count=${count}`)
  if (!response.ok) {
    throw new Error(`Failed to fetch git log: ${response.statusText}`)
  }
  return response.json()
}

export function useGit({
  projectName,
  autoConnect = false,
  onError,
  onStatusUpdate,
}: UseGitOptions): UseGitReturn {
  const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('disconnected')
  const [wsStatus, setWsStatus] = useState<GitStatus | null>(null)

  const wsRef = useRef<WebSocket | null>(null)
  const pingIntervalRef = useRef<number | null>(null)
  const reconnectTimeoutRef = useRef<number | null>(null)
  const reconnectAttempts = useRef(0)
  const maxReconnectAttempts = 3

  const queryClient = useQueryClient()

  // REST API queries
  const {
    data: restStatus,
    isLoading: statusLoading,
    refetch: refetchStatus,
  } = useQuery({
    queryKey: ['git-status', projectName],
    queryFn: () => fetchGitStatus(projectName),
    enabled: !!projectName,
    refetchInterval: false, // WebSocket handles real-time updates
    retry: 1,
  })

  const {
    data: commits = [],
    isLoading: commitsLoading,
    refetch: refetchCommits,
  } = useQuery({
    queryKey: ['git-log', projectName],
    queryFn: () => fetchGitLog(projectName),
    enabled: !!projectName,
    retry: 1,
  })

  // Prefer WebSocket status if connected, otherwise use REST
  const status = wsStatus || restStatus || null

  // Clean up on unmount
  useEffect(() => {
    return () => {
      if (pingIntervalRef.current) {
        clearInterval(pingIntervalRef.current)
      }
      if (reconnectTimeoutRef.current) {
        clearTimeout(reconnectTimeoutRef.current)
      }
      if (wsRef.current) {
        wsRef.current.close()
      }
    }
  }, [])

  // Auto-connect if enabled
  useEffect(() => {
    if (autoConnect && projectName) {
      connect()
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [autoConnect, projectName])

  const connect = useCallback(() => {
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      return
    }

    setConnectionStatus('connecting')

    const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
    const host = window.location.host
    const wsUrl = `${protocol}//${host}/api/git/projects/${encodeURIComponent(projectName)}/ws`

    const ws = new WebSocket(wsUrl)
    wsRef.current = ws

    ws.onopen = () => {
      setConnectionStatus('connected')
      reconnectAttempts.current = 0

      // Subscribe to status updates
      ws.send(JSON.stringify({ type: 'subscribe' }))

      // Start ping interval
      pingIntervalRef.current = window.setInterval(() => {
        if (ws.readyState === WebSocket.OPEN) {
          ws.send(JSON.stringify({ type: 'ping' }))
        }
      }, 30000)
    }

    ws.onclose = () => {
      setConnectionStatus('disconnected')
      if (pingIntervalRef.current) {
        clearInterval(pingIntervalRef.current)
        pingIntervalRef.current = null
      }

      // Attempt reconnection
      if (reconnectAttempts.current < maxReconnectAttempts) {
        reconnectAttempts.current++
        const delay = Math.min(1000 * Math.pow(2, reconnectAttempts.current), 10000)
        reconnectTimeoutRef.current = window.setTimeout(connect, delay)
      }
    }

    ws.onerror = () => {
      setConnectionStatus('error')
      onError?.('WebSocket connection error')
    }

    ws.onmessage = (event) => {
      try {
        const data = JSON.parse(event.data)

        switch (data.type) {
          case 'status': {
            const newStatus = data.data as GitStatus
            setWsStatus(newStatus)
            onStatusUpdate?.(newStatus)
            break
          }

          case 'error': {
            onError?.(data.content)
            break
          }

          case 'pong': {
            // Ping response - ignore
            break
          }

          default: {
            console.warn('Unknown git message type:', data.type)
          }
        }
      } catch (e) {
        console.error('Failed to parse git message:', e)
      }
    }
  }, [projectName, onError, onStatusUpdate])

  const disconnect = useCallback(() => {
    reconnectAttempts.current = maxReconnectAttempts // Prevent auto-reconnect
    if (reconnectTimeoutRef.current) {
      clearTimeout(reconnectTimeoutRef.current)
      reconnectTimeoutRef.current = null
    }
    if (pingIntervalRef.current) {
      clearInterval(pingIntervalRef.current)
      pingIntervalRef.current = null
    }
    if (wsRef.current) {
      // Unsubscribe before closing
      if (wsRef.current.readyState === WebSocket.OPEN) {
        wsRef.current.send(JSON.stringify({ type: 'unsubscribe' }))
      }
      wsRef.current.close()
      wsRef.current = null
    }
    setConnectionStatus('disconnected')
    setWsStatus(null)
  }, [])

  const refresh = useCallback(() => {
    // Refresh via WebSocket if connected
    if (wsRef.current?.readyState === WebSocket.OPEN) {
      wsRef.current.send(JSON.stringify({ type: 'refresh' }))
    } else {
      // Fall back to REST API
      refetchStatus()
      refetchCommits()
    }
  }, [refetchStatus, refetchCommits])

  // Mutation helpers
  const stageFilesMutation = useMutation({
    mutationFn: async (files?: string[]): Promise<GitResult> => {
      const response = await fetch(`${API_BASE}/${encodeURIComponent(projectName)}/add`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(files || null),
      })
      if (!response.ok) {
        throw new Error(`Stage failed: ${response.statusText}`)
      }
      return response.json()
    },
    onSuccess: () => {
      refresh()
    },
    onError: (error: Error) => {
      onError?.(error.message)
    },
  })

  const commitMutation = useMutation({
    mutationFn: async ({ message, files }: { message: string; files?: string[] }): Promise<GitResult> => {
      const response = await fetch(`${API_BASE}/${encodeURIComponent(projectName)}/commit`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ message, files }),
      })
      if (!response.ok) {
        throw new Error(`Commit failed: ${response.statusText}`)
      }
      return response.json()
    },
    onSuccess: () => {
      refresh()
      queryClient.invalidateQueries({ queryKey: ['git-log', projectName] })
    },
    onError: (error: Error) => {
      onError?.(error.message)
    },
  })

  const pushMutation = useMutation({
    mutationFn: async (options?: { remote?: string; branch?: string; force?: boolean; setUpstream?: boolean }): Promise<GitResult> => {
      const response = await fetch(`${API_BASE}/${encodeURIComponent(projectName)}/push`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(options || {}),
      })
      if (!response.ok) {
        throw new Error(`Push failed: ${response.statusText}`)
      }
      return response.json()
    },
    onSuccess: () => {
      refresh()
    },
    onError: (error: Error) => {
      onError?.(error.message)
    },
  })

  const pullMutation = useMutation({
    mutationFn: async (options?: { remote?: string; branch?: string; rebase?: boolean }): Promise<GitResult> => {
      const response = await fetch(`${API_BASE}/${encodeURIComponent(projectName)}/pull`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(options || {}),
      })
      if (!response.ok) {
        throw new Error(`Pull failed: ${response.statusText}`)
      }
      return response.json()
    },
    onSuccess: () => {
      refresh()
      queryClient.invalidateQueries({ queryKey: ['git-log', projectName] })
    },
    onError: (error: Error) => {
      onError?.(error.message)
    },
  })

  const fetchMutation = useMutation({
    mutationFn: async (remote: string = 'origin'): Promise<GitResult> => {
      const response = await fetch(`${API_BASE}/${encodeURIComponent(projectName)}/fetch?remote=${encodeURIComponent(remote)}`, {
        method: 'POST',
      })
      if (!response.ok) {
        throw new Error(`Fetch failed: ${response.statusText}`)
      }
      return response.json()
    },
    onSuccess: () => {
      refresh()
    },
    onError: (error: Error) => {
      onError?.(error.message)
    },
  })

  const checkoutMutation = useMutation({
    mutationFn: async ({ branch, create }: { branch: string; create?: boolean }): Promise<GitResult> => {
      const response = await fetch(`${API_BASE}/${encodeURIComponent(projectName)}/checkout`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ branch, create: create || false }),
      })
      if (!response.ok) {
        throw new Error(`Checkout failed: ${response.statusText}`)
      }
      return response.json()
    },
    onSuccess: () => {
      refresh()
      queryClient.invalidateQueries({ queryKey: ['git-log', projectName] })
    },
    onError: (error: Error) => {
      onError?.(error.message)
    },
  })

  const getDiff = useCallback(async (staged: boolean = false, file?: string): Promise<string> => {
    const params = new URLSearchParams()
    if (staged) params.set('staged', 'true')
    if (file) params.set('file', file)

    const response = await fetch(`${API_BASE}/${encodeURIComponent(projectName)}/diff?${params}`)
    if (!response.ok) {
      throw new Error(`Failed to get diff: ${response.statusText}`)
    }
    const data = await response.json()
    return data.diff
  }, [projectName])

  return {
    status,
    commits,
    isLoading: statusLoading || commitsLoading,
    connectionStatus,
    connect,
    disconnect,
    refresh,
    stageFiles: (files?: string[]) => stageFilesMutation.mutateAsync(files),
    commit: (message: string, files?: string[]) => commitMutation.mutateAsync({ message, files }),
    push: (options) => pushMutation.mutateAsync(options),
    pull: (options) => pullMutation.mutateAsync(options),
    fetch: (remote) => fetchMutation.mutateAsync(remote),
    checkout: (branch: string, create?: boolean) => checkoutMutation.mutateAsync({ branch, create }),
    getDiff,
  }
}


Now let me create the GitPanel.tsx component. It should have:
- Branch indicator
- Changed files list (staged/unstaged/untracked)
- Commit history (recent)
- Push/pull buttons
- Auto-refresh on file changes via WebSocket


Now let me create the GitPanel component.


/**
 * Git Panel Component
 *
 * Displays git status, changed files, commit history, and provides
 * controls for common git operations (commit, push, pull).
 */

import { useState, useEffect, useCallback } from 'react'
import {
  GitBranch,
  GitCommit,
  GitPullRequest,
  Upload,
  Download,
  RefreshCw,
  Plus,
  Minus,
  FileQuestion,
  AlertTriangle,
  Check,
  ChevronDown,
  ChevronRight,
  X,
  Send,
} from 'lucide-react'
import { useGit, GitStatus, GitCommit as GitCommitType } from '../hooks/useGit'

interface GitPanelProps {
  projectName: string
  isOpen: boolean
  onClose: () => void
}

type FileCategory = 'staged' | 'unstaged' | 'untracked'

export function GitPanel({ projectName, isOpen, onClose }: GitPanelProps) {
  const [expandedSections, setExpandedSections] = useState<Record<string, boolean>>({
    staged: true,
    unstaged: true,
    untracked: true,
    history: false,
  })
  const [commitMessage, setCommitMessage] = useState('')
  const [isCommitting, setIsCommitting] = useState(false)
  const [isPushing, setIsPushing] = useState(false)
  const [isPulling, setIsPulling] = useState(false)
  const [error, setError] = useState<string | null>(null)
  const [successMessage, setSuccessMessage] = useState<string | null>(null)

  const {
    status,
    commits,
    isLoading,
    connectionStatus,
    connect,
    disconnect,
    refresh,
    stageFiles,
    commit,
    push,
    pull,
  } = useGit({
    projectName,
    autoConnect: isOpen,
    onError: (err) => {
      setError(err)
      setTimeout(() => setError(null), 5000)
    },
  })

  // Connect/disconnect based on panel state
  useEffect(() => {
    if (isOpen) {
      connect()
    } else {
      disconnect()
    }
  }, [isOpen, connect, disconnect])

  // Clear messages after a delay
  useEffect(() => {
    if (successMessage) {
      const timer = setTimeout(() => setSuccessMessage(null), 3000)
      return () => clearTimeout(timer)
    }
  }, [successMessage])

  const toggleSection = useCallback((section: string) => {
    setExpandedSections((prev) => ({
      ...prev,
      [section]: !prev[section],
    }))
  }, [])

  const handleStageAll = useCallback(async () => {
    try {
      await stageFiles()
      setSuccessMessage('All files staged')
    } catch (e) {
      // Error handled by hook
    }
  }, [stageFiles])

  const handleStageFile = useCallback(async (file: string) => {
    try {
      await stageFiles([file])
    } catch (e) {
      // Error handled by hook
    }
  }, [stageFiles])

  const handleCommit = useCallback(async () => {
    if (!commitMessage.trim()) {
      setError('Please enter a commit message')
      return
    }

    setIsCommitting(true)
    try {
      const result = await commit(commitMessage.trim())
      if (result.success) {
        setCommitMessage('')
        setSuccessMessage(`Committed: ${result.commit_hash?.substring(0, 7)}`)
      } else {
        setError(result.error || 'Commit failed')
      }
    } catch (e) {
      // Error handled by hook
    } finally {
      setIsCommitting(false)
    }
  }, [commit, commitMessage])

  const handlePush = useCallback(async () => {
    setIsPushing(true)
    try {
      const result = await push()
      if (result.success) {
        setSuccessMessage('Pushed to remote')
      } else {
        setError(result.error || 'Push failed')
      }
    } catch (e) {
      // Error handled by hook
    } finally {
      setIsPushing(false)
    }
  }, [push])

  const handlePull = useCallback(async () => {
    setIsPulling(true)
    try {
      const result = await pull()
      if (result.success) {
        setSuccessMessage('Pulled from remote')
      } else {
        setError(result.error || 'Pull failed')
      }
    } catch (e) {
      // Error handled by hook
    } finally {
      setIsPulling(false)
    }
  }, [pull])

  const getFileIcon = (category: FileCategory) => {
    switch (category) {
      case 'staged':
        return <Plus size={14} className="text-[var(--color-neo-done)]" />
      case 'unstaged':
        return <Minus size={14} className="text-[var(--color-neo-danger)]" />
      case 'untracked':
        return <FileQuestion size={14} className="text-[var(--color-neo-text-secondary)]" />
    }
  }

  const getStatusColor = () => {
    if (!status) return 'bg-gray-400'
    if (status.has_conflicts) return 'bg-[var(--color-neo-danger)]'
    if (status.is_clean) return 'bg-[var(--color-neo-done)]'
    return 'bg-[var(--color-neo-pending)]'
  }

  const getConnectionBadge = () => {
    switch (connectionStatus) {
      case 'connected':
        return (

            Live

        )
      case 'connecting':
        return (

            <RefreshCw size={12} className="animate-spin" />
            Connecting

        )
      case 'error':
        return (

            <AlertTriangle size={12} />
            Error

        )
      default:
        return (

            Offline

        )
    }
  }

  const totalChanges = status
    ? status.staged.length + status.unstaged.length + status.untracked.length
    : 0

  return (
    <>
      {/* Backdrop */}
      {isOpen && (

      )}

      {/* Panel */}

        {/* Header */}

            <div className={`p-1.5 border-2 border-white ${getStatusColor()}`}>
              <GitBranch size={18} className="text-white" />

                Git Status
                {status?.has_conflicts && (
                  <AlertTriangle size={16} className="text-[var(--color-neo-danger)]" />
                )}

                {status?.branch || 'Loading...'}
                {status && status.ahead > 0 && (
                  +{status.ahead}
                )}
                {status && status.behind > 0 && (
                  -{status.behind}
                )}

            {getConnectionBadge()}
            <button
              onClick={refresh}
              className="neo-btn neo-btn-ghost p-2 bg-white/20 border-white/40 hover:bg-white/30 text-white"
              title="Refresh"
              disabled={isLoading}
            >
              <RefreshCw size={16} className={isLoading ? 'animate-spin' : ''} />
            </button>
            <button
              onClick={onClose}
              className="neo-btn neo-btn-ghost p-2 bg-white/20 border-white/40 hover:bg-white/30 text-white"
              title="Close"
            >
              <X size={18} />
            </button>

        {/* Messages */}
        {error && (

            {error}

        )}
        {successMessage && (

            <Check size={14} />
            {successMessage}

        )}

        {/* Content */}

          {isLoading && !status ? (

              <RefreshCw size={24} className="animate-spin text-[var(--color-neo-progress)]" />

          ) : status ? (

              {/* Quick Actions */}

                <button
                  onClick={handlePull}
                  disabled={isPulling}
                  className="neo-btn flex-1 py-2 px-3 text-xs"
                >
                  <Download size={14} />
                  {isPulling ? 'Pulling...' : 'Pull'}
                </button>
                <button
                  onClick={handlePush}
                  disabled={isPushing || (status.ahead === 0 && status.is_clean)}
                  className="neo-btn neo-btn-primary flex-1 py-2 px-3 text-xs"
                >
                  <Upload size={14} />
                  {isPushing ? 'Pushing...' : 'Push'}
                  {status.ahead > 0 && (
                    {status.ahead}
                  )}
                </button>

              {/* Status Summary */}
              {status.is_clean ? (

                    <Check size={18} className="text-[var(--color-neo-done)]" />
                    Working tree clean

              ) : (
                <>
                  {/* Staged Files */}
                  {status.staged.length > 0 && (
                    <FileSection
                      title="Staged Changes"
                      files={status.staged}
                      category="staged"
                      isExpanded={expandedSections.staged}
                      onToggle={() => toggleSection('staged')}
                      getIcon={getFileIcon}
                    />
                  )}

                  {/* Unstaged Files */}
                  {status.unstaged.length > 0 && (
                    <FileSection
                      title="Changes"
                      files={status.unstaged}
                      category="unstaged"
                      isExpanded={expandedSections.unstaged}
                      onToggle={() => toggleSection('unstaged')}
                      getIcon={getFileIcon}
                      onStageFile={handleStageFile}
                    />
                  )}

                  {/* Untracked Files */}
                  {status.untracked.length > 0 && (
                    <FileSection
                      title="Untracked Files"
                      files={status.untracked}
                      category="untracked"
                      isExpanded={expandedSections.untracked}
                      onToggle={() => toggleSection('untracked')}
                      getIcon={getFileIcon}
                      onStageFile={handleStageFile}
                    />
                  )}

                  {/* Stage All Button */}
                  {(status.unstaged.length > 0 || status.untracked.length > 0) && (
                    <button
                      onClick={handleStageAll}
                      className="neo-btn w-full py-2 text-xs"
                    >
                      <Plus size={14} />
                      Stage All Changes
                    </button>
                  )}

                  {/* Commit Form */}
                  {status.staged.length > 0 && (

                        <GitCommit size={14} />
                        Commit {status.staged.length} staged file{status.staged.length !== 1 ? 's' : ''}

                      <textarea
                        value={commitMessage}
                        onChange={(e) => setCommitMessage(e.target.value)}
                        placeholder="Commit message..."
                        className="neo-input text-sm font-mono resize-none"
                        rows={2}
                        onKeyDown={(e) => {
                          if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
                            handleCommit()
                          }
                        }}
                      />
                      <button
                        onClick={handleCommit}
                        disabled={isCommitting || !commitMessage.trim()}
                        className="neo-btn neo-btn-success w-full py-2 text-xs"
                      >
                        <Send size={14} />
                        {isCommitting ? 'Committing...' : 'Commit'}
                      </button>

                  )}
                </>
              )}

              {/* Commit History */}

                <button
                  onClick={() => toggleSection('history')}
                  className="flex items-center justify-between w-full text-left"
                >

                    <GitPullRequest size={14} />
                    Recent Commits

                  {expandedSections.history ? (
                    <ChevronDown size={16} />
                  ) : (
                    <ChevronRight size={16} />
                  )}
                </button>

                {expandedSections.history && (

                    {commits.length === 0 ? (

                        No commits yet

                    ) : (
                      commits.slice(0, 5).map((c) => (
                        <CommitEntry key={c.hash} commit={c} />
                      ))
                    )}

                )}

          ) : (

              Not a git repository

          )}

    </>
  )
}

// File Section Component
interface FileSectionProps {
  title: string
  files: string[]
  category: FileCategory
  isExpanded: boolean
  onToggle: () => void
  getIcon: (category: FileCategory) => React.ReactNode
  onStageFile?: (file: string) => void
}

function FileSection({
  title,
  files,
  category,
  isExpanded,
  onToggle,
  getIcon,
  onStageFile,
}: FileSectionProps) {
  const categoryColors: Record<FileCategory, string> = {
    staged: 'border-[var(--color-neo-done)]',
    unstaged: 'border-[var(--color-neo-pending)]',
    untracked: 'border-gray-300',
  }

  return (
    <div className={`neo-card p-0 overflow-hidden ${categoryColors[category]}`}>
      <button
        onClick={onToggle}
        className="flex items-center justify-between w-full p-3 text-left hover:bg-gray-50"
      >

          {getIcon(category)}
          {title}
          {files.length}

        {isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
      </button>

      {isExpanded && (

          {files.map((file) => (

                {file}

              {onStageFile && (
                <button
                  onClick={() => onStageFile(file)}
                  className="ml-2 p-1 hover:bg-[var(--color-neo-done)]/20 rounded"
                  title="Stage file"
                >
                  <Plus size={12} className="text-[var(--color-neo-done)]" />
                </button>
              )}

          ))}

      )}

  )
}

// Commit Entry Component
interface CommitEntryProps {
  commit: GitCommitType
}

function CommitEntry({ commit }: CommitEntryProps) {
  return (

          {commit.short_hash}

            {commit.message}

            {commit.author} - {formatDate(commit.date)}

  )
}

// Helper function to format dates
function formatDate(dateStr: string): string {
  try {
    const date = new Date(dateStr)
    const now = new Date()
    const diffMs = now.getTime() - date.getTime()
    const diffMins = Math.floor(diffMs / 60000)
    const diffHours = Math.floor(diffMs / 3600000)
    const diffDays = Math.floor(diffMs / 86400000)

    if (diffMins < 1) return 'just now'
    if (diffMins < 60) return `${diffMins}m ago`
    if (diffHours < 24) return `${diffHours}h ago`
    if (diffDays < 7) return `${diffDays}d ago`

    return date.toLocaleDateString()
  } catch {
    return dateStr
  }
}