import React, { useEffect, useRef, useState, useCallback } from 'react'; import { Terminal } from 'xterm'; import { FitAddon } from '@xterm/addon-fit'; import { WebLinksAddon } from '@xterm/addon-web-links'; import 'xterm/css/xterm.css'; import { Terminal as TerminalIcon, Maximize2, Minimize2, X, RefreshCw, AlertCircle, FolderOpen, Plus } from 'lucide-react'; import { useStudy } from '../context/StudyContext'; import { useClaudeTerminal } from '../context/ClaudeTerminalContext'; interface ClaudeTerminalProps { isExpanded?: boolean; onToggleExpand?: () => void; onClose?: () => void; } export const ClaudeTerminal: React.FC = ({ isExpanded = false, onToggleExpand, onClose }) => { const { selectedStudy } = useStudy(); const { setIsConnected: setGlobalConnected } = useClaudeTerminal(); const terminalRef = useRef(null); const xtermRef = useRef(null); const fitAddonRef = useRef(null); const wsRef = useRef(null); const [isConnected, setIsConnectedLocal] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const [_error, setError] = useState(null); const [cliAvailable, setCliAvailable] = useState(null); const [contextSet, setContextSet] = useState(false); const [settingContext, setSettingContext] = useState(false); // Sync local connection state to global context const setIsConnected = useCallback((connected: boolean) => { setIsConnectedLocal(connected); setGlobalConnected(connected); }, [setGlobalConnected]); // Check CLI availability useEffect(() => { fetch('/api/terminal/status') .then(res => res.json()) .then(data => { setCliAvailable(data.available); if (!data.available) { setError(data.message); } }) .catch(() => { setCliAvailable(false); setError('Could not check Claude Code CLI status'); }); }, []); // Initialize terminal useEffect(() => { if (!terminalRef.current || xtermRef.current) return; const term = new Terminal({ cursorBlink: true, fontSize: 13, fontFamily: '"JetBrains Mono", "Fira Code", Consolas, monospace', theme: { background: '#0f172a', foreground: '#e2e8f0', cursor: '#60a5fa', cursorAccent: '#0f172a', selectionBackground: '#334155', black: '#1e293b', red: '#ef4444', green: '#22c55e', yellow: '#eab308', blue: '#3b82f6', magenta: '#a855f7', cyan: '#06b6d4', white: '#f1f5f9', brightBlack: '#475569', brightRed: '#f87171', brightGreen: '#4ade80', brightYellow: '#facc15', brightBlue: '#60a5fa', brightMagenta: '#c084fc', brightCyan: '#22d3ee', brightWhite: '#f8fafc', }, allowProposedApi: true, }); const fitAddon = new FitAddon(); const webLinksAddon = new WebLinksAddon(); term.loadAddon(fitAddon); term.loadAddon(webLinksAddon); term.open(terminalRef.current); // Initial fit setTimeout(() => fitAddon.fit(), 0); xtermRef.current = term; fitAddonRef.current = fitAddon; // Welcome message term.writeln('\x1b[1;36m╔══════════════════════════════════════════════════════════╗\x1b[0m'); term.writeln('\x1b[1;36m║\x1b[0m \x1b[1;37mClaude Code Terminal\x1b[0m \x1b[1;36m║\x1b[0m'); term.writeln('\x1b[1;36m║\x1b[0m \x1b[90mFull Claude Code experience in the Atomizer dashboard\x1b[0m \x1b[1;36m║\x1b[0m'); term.writeln('\x1b[1;36m╚══════════════════════════════════════════════════════════╝\x1b[0m'); term.writeln(''); if (cliAvailable === false) { term.writeln('\x1b[1;31mError:\x1b[0m Claude Code CLI not found.'); term.writeln('Install with: \x1b[1;33mnpm install -g @anthropic-ai/claude-code\x1b[0m'); } else { term.writeln('\x1b[90mClick "Connect" to start a Claude Code session.\x1b[0m'); term.writeln('\x1b[90mClaude will have access to CLAUDE.md and .claude/ skills.\x1b[0m'); } term.writeln(''); return () => { term.dispose(); xtermRef.current = null; }; }, [cliAvailable]); // Handle resize useEffect(() => { const handleResize = () => { if (fitAddonRef.current) { fitAddonRef.current.fit(); // Send resize to backend if (wsRef.current?.readyState === WebSocket.OPEN && xtermRef.current) { wsRef.current.send(JSON.stringify({ type: 'resize', cols: xtermRef.current.cols, rows: xtermRef.current.rows })); } } }; window.addEventListener('resize', handleResize); // Also fit when expanded state changes setTimeout(handleResize, 100); return () => window.removeEventListener('resize', handleResize); }, [isExpanded]); // Connect to terminal WebSocket const connect = useCallback(() => { if (!xtermRef.current || wsRef.current?.readyState === WebSocket.OPEN) return; setIsConnecting(true); setError(null); // Let backend determine the working directory (ATOMIZER_ROOT) // Pass study_id as parameter so we can inform Claude about the context const studyParam = selectedStudy?.id ? `?study_id=${selectedStudy.id}` : ''; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const ws = new WebSocket(`${protocol}//${window.location.host}/api/terminal/claude${studyParam}`); ws.onopen = () => { setIsConnected(true); setIsConnecting(false); xtermRef.current?.clear(); xtermRef.current?.writeln('\x1b[1;32mConnected to Claude Code\x1b[0m'); if (selectedStudy?.id) { xtermRef.current?.writeln(`\x1b[90mStudy: \x1b[1;33m${selectedStudy.id}\x1b[0m \x1b[90m- Click "Set Context" to initialize\x1b[0m`); } xtermRef.current?.writeln(''); // Send initial resize if (xtermRef.current) { ws.send(JSON.stringify({ type: 'resize', cols: xtermRef.current.cols, rows: xtermRef.current.rows })); } }; ws.onmessage = (event) => { try { const message = JSON.parse(event.data); switch (message.type) { case 'output': xtermRef.current?.write(message.data); break; case 'started': xtermRef.current?.writeln(`\x1b[90m${message.message}\x1b[0m`); break; case 'exit': xtermRef.current?.writeln(''); xtermRef.current?.writeln(`\x1b[33mClaude Code exited with code ${message.code}\x1b[0m`); setIsConnected(false); break; case 'error': xtermRef.current?.writeln(`\x1b[1;31mError: ${message.message}\x1b[0m`); setError(message.message); break; } } catch (e) { // Raw output xtermRef.current?.write(event.data); } }; ws.onerror = () => { setError('WebSocket connection error'); setIsConnecting(false); }; ws.onclose = () => { setIsConnected(false); setIsConnecting(false); xtermRef.current?.writeln(''); xtermRef.current?.writeln('\x1b[90mDisconnected from Claude Code\x1b[0m'); }; // Handle terminal input const disposable = xtermRef.current.onData((data) => { if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'input', data })); } }); wsRef.current = ws; return () => { disposable.dispose(); }; }, [selectedStudy]); // Disconnect const disconnect = useCallback(() => { if (wsRef.current) { wsRef.current.send(JSON.stringify({ type: 'stop' })); wsRef.current.close(); wsRef.current = null; } setIsConnected(false); setContextSet(false); }, []); // Set study context - sends context message to Claude silently const setStudyContext = useCallback(() => { if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) return; setSettingContext(true); let contextMessage: string; if (selectedStudy?.id) { // Existing study context contextMessage = `You are helping with Atomizer optimization. ` + `First read: .claude/skills/00_BOOTSTRAP.md for task routing. ` + `Then follow the Protocol Execution Framework. ` + `Study context: Working on "${selectedStudy.id}" at studies/${selectedStudy.id}/. ` + `Use atomizer conda env. Acknowledge briefly.`; } else { // No study selected - offer to create new study contextMessage = `You are helping with Atomizer optimization. ` + `First read: .claude/skills/00_BOOTSTRAP.md for task routing. ` + `No study is currently selected. ` + `Read .claude/skills/guided-study-wizard.md and help the user create a new optimization study. ` + `Use atomizer conda env. Start the guided wizard by asking what they want to optimize.`; } wsRef.current.send(JSON.stringify({ type: 'input', data: contextMessage + '\n' })); // Mark as done after Claude has had time to process setTimeout(() => { setSettingContext(false); setContextSet(true); }, 500); }, [selectedStudy]); // Cleanup on unmount useEffect(() => { return () => { if (wsRef.current) { wsRef.current.close(); } }; }, []); return (
{/* Header */}
Claude Code {selectedStudy && ( {selectedStudy.id} )}
{/* Connection status indicator */}
{/* Connect/Disconnect button */} {/* Set Context button - works for both existing study and new study creation */} {onToggleExpand && ( )} {onClose && ( )}
{/* CLI not available warning */} {cliAvailable === false && (
Claude Code CLI not found. Install with: npm install -g @anthropic-ai/claude-code
)} {/* Terminal */}
{/* Footer */}

Claude Code has access to CLAUDE.md instructions and .claude/ skills for Atomizer optimization

); }; export default ClaudeTerminal;