diff --git a/server/index.js b/server/index.js index cb454bd..d22c3a9 100644 --- a/server/index.js +++ b/server/index.js @@ -11,6 +11,17 @@ const __dirname = path.dirname(__filename); const app = express(); const PORT = process.env.PORT || 3002; +// CORS middleware +app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', '*'); + res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Content-Type, Accept'); + if (req.method === 'OPTIONS') { + return res.status(204).send(); + } + next(); +}); + // Middleware app.use(express.json({ limit: '50mb' })); app.use(express.text({ type: '*/*' })); diff --git a/src/App.tsx b/src/App.tsx index 1da7e28..737d83f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,27 +8,26 @@ import { closeTerminal, } from './lib/terminalApi'; import { getDefaultTheme, type TerminalTheme } from './lib/terminalTheme'; -import { useTerminalStore, type TerminalChunk } from './stores/useTerminalStore'; +import { type TerminalChunk } from './stores/useTerminalStore'; const DEFAULT_CWD = '/workspace'; const DEFAULT_FONT_SIZE = 14; const DEFAULT_FONT_FAMILY = 'IBM Plex Mono'; -function App() { - const [isConnected, setIsConnected] = useState(false); - const [isConnecting, setIsConnecting] = useState(false); - const [error, setError] = useState(null); - const [chunks, setChunks] = useState([]); +function TerminalPanel({ initialCwd, connectDelay = 0 }: { initialCwd: string; connectDelay?: number }) { const [theme] = useState(getDefaultTheme()); const [fontSize] = useState(DEFAULT_FONT_SIZE); const [fontFamily] = useState(DEFAULT_FONT_FAMILY); - const [cwd, setCwd] = useState(DEFAULT_CWD); - const [inputCwd, setInputCwd] = useState(DEFAULT_CWD); + const [chunks, setChunks] = useState([]); + const [isConnected, setIsConnected] = useState(false); + const [isConnecting, setIsConnecting] = useState(false); + const [error, setError] = useState(null); const sessionIdRef = useRef(null); const cleanupRef = useRef<(() => void) | null>(null); const nextChunkIdRef = useRef(1); const isMountedRef = useRef(true); + const hasConnectedRef = useRef(false); const handleInput = useCallback(async (data: string) => { const sessionId = sessionIdRef.current; @@ -52,122 +51,112 @@ function App() { } }, []); - const connectToTerminal = useCallback(async (targetCwd: string) => { - // Cleanup previous session - if (cleanupRef.current) { - cleanupRef.current(); - cleanupRef.current = null; - } + useEffect(() => { + // Skip if already connected (e.g., React strict mode double-mount) + if (hasConnectedRef.current) return; - if (sessionIdRef.current) { - try { - await closeTerminal(sessionIdRef.current); - } catch { - // Ignore cleanup errors + isMountedRef.current = true; + + const connectToTerminal = async (targetCwd: string) => { + // Cleanup previous session + if (cleanupRef.current) { + cleanupRef.current(); + cleanupRef.current = null; } + + if (sessionIdRef.current) { + try { + await closeTerminal(sessionIdRef.current); + } catch { + // Ignore cleanup errors + } + } + + setIsConnecting(true); + setError(null); + setChunks([]); sessionIdRef.current = null; - } + nextChunkIdRef.current = 1; - setIsConnecting(true); - setError(null); - setChunks([]); - nextChunkIdRef.current = 1; + try { + const session = await createTerminalSession({ + cwd: targetCwd, + cols: 80, + rows: 24, + }); - try { - const session = await createTerminalSession({ - cwd: targetCwd, - cols: 80, - rows: 24, - }); + if (!isMountedRef.current) return; - if (!isMountedRef.current) return; + sessionIdRef.current = session.sessionId; - sessionIdRef.current = session.sessionId; + cleanupRef.current = connectTerminalStream( + session.sessionId, + (event) => { + if (!isMountedRef.current) return; - cleanupRef.current = connectTerminalStream( - session.sessionId, - (event) => { - if (!isMountedRef.current) return; - - if (event.type === 'connected') { - setIsConnected(true); - setIsConnecting(false); - } else if (event.type === 'data' && event.data) { - setChunks((prev) => { + if (event.type === 'connected') { + setIsConnected(true); + setIsConnecting(false); + } else if (event.type === 'data' && event.data) { const newChunk: TerminalChunk = { id: nextChunkIdRef.current++, - data: event.data!, + data: event.data, }; - return [...prev, newChunk]; - }); - } else if (event.type === 'exit') { - setIsConnected(false); - if (event.exitCode !== 0) { - setError(`Terminal exited with code ${event.exitCode}`); + setChunks((prev) => [...prev, newChunk]); + } else if (event.type === 'exit') { + setIsConnected(false); + if (event.exitCode !== 0) { + setError(`Terminal exited with code ${event.exitCode}`); + } + } else if (event.type === 'reconnecting') { + setIsConnecting(true); } - } else if (event.type === 'reconnecting') { - setIsConnecting(true); + }, + (err, fatal) => { + console.error('Terminal error:', err); + setIsConnected(false); + if (fatal) { + setError(err.message); + } + setIsConnecting(false); } - }, - (err, fatal) => { - console.error('Terminal error:', err); - setIsConnected(false); - if (fatal) { - setError(err.message); - } - setIsConnecting(false); - } - ); - } catch (err) { - if (!isMountedRef.current) return; - setIsConnecting(false); - setError(err instanceof Error ? err.message : 'Failed to create terminal'); - } - }, []); - - const handleConnect = useCallback(() => { - if (inputCwd.trim()) { - setCwd(inputCwd.trim()); - connectToTerminal(inputCwd.trim()); - } - }, [inputCwd, connectToTerminal]); - - const handleDisconnect = useCallback(async () => { - if (cleanupRef.current) { - cleanupRef.current(); - cleanupRef.current = null; - } - - if (sessionIdRef.current) { - try { - await closeTerminal(sessionIdRef.current); - } catch { - // Ignore cleanup errors + ); + } catch (err) { + if (!isMountedRef.current) return; + setIsConnecting(false); + setError(err instanceof Error ? err.message : 'Failed to create terminal'); } - sessionIdRef.current = null; - } + }; - setIsConnected(false); - setChunks([]); - }, []); - - useEffect(() => { - isMountedRef.current = true; - connectToTerminal(DEFAULT_CWD); + // Apply connection delay to stagger connections + const delay = connectDelay > 0 ? connectDelay : 0; + const timeoutId = setTimeout(() => { + hasConnectedRef.current = true; + connectToTerminal(initialCwd); + }, delay); return () => { + clearTimeout(timeoutId); isMountedRef.current = false; if (cleanupRef.current) { cleanupRef.current(); } }; - }, [connectToTerminal]); + }, [initialCwd, connectDelay]); return ( -
+
+ {/* Panel header */} +
+ {initialCwd} + + {isConnected ? 'Connected' : isConnecting ? 'Connecting...' : 'Not connected'} + +
+ {/* Error message */} {error && ( -
+
{error}
)} @@ -176,7 +165,7 @@ function App() {
{isConnected ? ( ) : ( -
- {isConnecting ? 'Connecting to terminal...' : 'Click "Connect" to start a terminal session'} +
+ {isConnecting ? 'Connecting...' : 'Click to connect'}
)}
- - {/* Footer */} -
- {cwd} - {isConnected ? 'Connected' : 'Not connected'} -
+
+ ); +} + +function App() { + return ( +
+ + + + + +
); } diff --git a/vite.config.ts b/vite.config.ts index 558e986..0c99825 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -21,6 +21,8 @@ export default defineConfig({ '/api': { target: 'http://localhost:3002', changeOrigin: true, + ws: true, + rewrite: (path) => path, }, }, },