feat: add interactive terminal and dev server management

Add new features for interactive terminal sessions and dev server control:

Terminal Component:
- New Terminal.tsx component using xterm.js for full terminal emulation
- WebSocket-based PTY communication with bidirectional I/O
- Cross-platform support (Windows via winpty, Unix via built-in pty)
- Auto-reconnection with exponential backoff
- Fix duplicate WebSocket connection bug by checking CONNECTING state
- Add manual close flag to prevent auto-reconnect race conditions
- Add project tracking to avoid duplicate connects on initial activation

Dev Server Management:
- New DevServerControl.tsx for starting/stopping dev servers
- DevServerManager service for subprocess management
- WebSocket streaming of dev server output
- Project configuration service for reading package.json scripts

Backend Infrastructure:
- Terminal router with WebSocket endpoint for PTY I/O
- DevServer router for server lifecycle management
- Terminal session manager with callback-based output streaming
- Enhanced WebSocket schemas for terminal and dev server messages

UI Integration:
- New Terminal and Dev Server tabs in the main application
- Updated DebugLogViewer with improved UI and functionality
- Extended useWebSocket hook for terminal message handling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Auto
2026-01-12 10:35:36 +02:00
parent b1473cdfb9
commit c1985eb285
22 changed files with 3360 additions and 66 deletions

View File

@@ -0,0 +1,155 @@
import { Globe, Square, Loader2, ExternalLink, AlertTriangle } from 'lucide-react'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import type { DevServerStatus } from '../lib/types'
import { startDevServer, stopDevServer } from '../lib/api'
// Re-export DevServerStatus from lib/types for consumers that import from here
export type { DevServerStatus }
// ============================================================================
// React Query Hooks (Internal)
// ============================================================================
/**
* Internal hook to start the dev server for a project.
* Invalidates the dev-server-status query on success.
*/
function useStartDevServer(projectName: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: () => startDevServer(projectName),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['dev-server-status', projectName] })
},
})
}
/**
* Internal hook to stop the dev server for a project.
* Invalidates the dev-server-status query on success.
*/
function useStopDevServer(projectName: string) {
const queryClient = useQueryClient()
return useMutation({
mutationFn: () => stopDevServer(projectName),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['dev-server-status', projectName] })
},
})
}
// ============================================================================
// Component
// ============================================================================
interface DevServerControlProps {
projectName: string
status: DevServerStatus
url: string | null
}
/**
* DevServerControl provides start/stop controls for a project's development server.
*
* Features:
* - Toggle button to start/stop the dev server
* - Shows loading state during operations
* - Displays clickable URL when server is running
* - Uses neobrutalism design with cyan accent when running
*/
export function DevServerControl({ projectName, status, url }: DevServerControlProps) {
const startDevServerMutation = useStartDevServer(projectName)
const stopDevServerMutation = useStopDevServer(projectName)
const isLoading = startDevServerMutation.isPending || stopDevServerMutation.isPending
const handleStart = () => {
// Clear any previous errors before starting
stopDevServerMutation.reset()
startDevServerMutation.mutate()
}
const handleStop = () => {
// Clear any previous errors before stopping
startDevServerMutation.reset()
stopDevServerMutation.mutate()
}
// Server is stopped when status is 'stopped' or 'crashed' (can restart)
const isStopped = status === 'stopped' || status === 'crashed'
// Server is in a running state
const isRunning = status === 'running'
// Server has crashed
const isCrashed = status === 'crashed'
return (
<div className="flex items-center gap-2">
{isStopped ? (
<button
onClick={handleStart}
disabled={isLoading}
className="neo-btn text-sm py-2 px-3"
style={isCrashed ? {
backgroundColor: 'var(--color-neo-danger)',
color: '#ffffff',
} : undefined}
title={isCrashed ? "Dev Server Crashed - Click to Restart" : "Start Dev Server"}
aria-label={isCrashed ? "Restart Dev Server (crashed)" : "Start Dev Server"}
>
{isLoading ? (
<Loader2 size={18} className="animate-spin" />
) : isCrashed ? (
<AlertTriangle size={18} />
) : (
<Globe size={18} />
)}
</button>
) : (
<button
onClick={handleStop}
disabled={isLoading}
className="neo-btn text-sm py-2 px-3"
style={{
backgroundColor: 'var(--color-neo-progress)',
color: '#ffffff',
}}
title="Stop Dev Server"
aria-label="Stop Dev Server"
>
{isLoading ? (
<Loader2 size={18} className="animate-spin" />
) : (
<Square size={18} />
)}
</button>
)}
{/* Show URL as clickable link when server is running */}
{isRunning && url && (
<a
href={url}
target="_blank"
rel="noopener noreferrer"
className="neo-btn text-sm py-2 px-3 gap-1"
style={{
backgroundColor: 'var(--color-neo-progress)',
color: '#ffffff',
textDecoration: 'none',
}}
title={`Open ${url} in new tab`}
>
<span className="font-mono text-xs">{url}</span>
<ExternalLink size={14} />
</a>
)}
{/* Error display */}
{(startDevServerMutation.error || stopDevServerMutation.error) && (
<span className="text-xs font-mono text-[var(--color-neo-danger)] ml-2">
{String((startDevServerMutation.error || stopDevServerMutation.error)?.message || 'Operation failed')}
</span>
)}
</div>
)
}