diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index 3528204..ec71019 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -3,604 +3,866 @@ import './App.css';
// UUID v4 generator — works in all contexts (HTTP and HTTPS)
function generateUUID() {
- // Use crypto.randomUUID if available (HTTPS only)
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
}
- // Fallback: manual UUID v4 using crypto.getRandomValues or Math.random
if (typeof crypto !== 'undefined' && typeof crypto.getRandomValues === 'function') {
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
- bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
- bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 1
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
return `${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`;
}
- // Last resort: Math.random-based UUID
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
const r = (Math.random() * 16) | 0;
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
});
}
+const API = '';
+
const App = () => {
- const [tasks, setTasks] = useState([]);
- const [selectedTask, setSelectedTask] = useState(null);
- const [showForm, setShowForm] = useState(false);
- const [formData, setFormData] = useState({
- name: '', description: '', prompt: '', schedule_type: 'manual',
- schedule_value: '', enabled: true,
- agent_model: '', agent_tools: 'Bash,Read,Write,Edit',
- agent_system_prompt: '', agent_permission_mode: 'auto', agent_timeout: 300
- });
- const [systemInfo, setSystemInfo] = useState(null);
- const [usageStats, setUsageStats] = useState(null);
- const [authStatus, setAuthStatus] = useState(null);
- const [mcpServers, setMcpServers] = useState(null);
- const [showAddMcp, setShowAddMcp] = useState(false);
- const [mcpForm, setMcpForm] = useState({ name: '', server_type: 'sse', url: '', command: '' });
- const [loading, setLoading] = useState(false);
- const [activeView, setActiveView] = useState('chat'); // 'chat' | 'tasks' | 'dashboard'
+ const [activeView, setActiveView] = useState('chat');
- // Auth
- const [authToken, setAuthToken] = useState('');
- const [tokenType, setTokenType] = useState('oauth_token');
- const [tokenSubmitting, setTokenSubmitting] = useState(false);
+ // Claude process status
+ const [claudeStatus, setClaudeStatus] = useState('unknown');
+ const [claudeSessionId, setClaudeSessionId] = useState(null);
- // Chat
+ // Chat state
const [chatMessages, setChatMessages] = useState([]);
const [chatInput, setChatInput] = useState('');
- const [chatSending, setChatSending] = useState(false);
- const [chatSessionId, setChatSessionId] = useState(() => generateUUID());
+ const [chatWaiting, setChatWaiting] = useState(false);
+ const [chatSessionId] = useState(() => generateUUID());
const [chatSessions, setChatSessions] = useState([]);
- const [chatModel, setChatModel] = useState('');
+ const [selectedHistorySession, setSelectedHistorySession] = useState(null);
const chatEndRef = useRef(null);
const wsRef = useRef(null);
const [wsConnected, setWsConnected] = useState(false);
+ const [wsError, setWsError] = useState(null);
+ const pendingAssistantRef = useRef('');
+ const reconnectTimerRef = useRef(null);
+
+ // Tasks state
+ const [tasks, setTasks] = useState([]);
+ const [showTaskForm, setShowTaskForm] = useState(false);
+ const [taskForm, setTaskForm] = useState({
+ name: '', description: '', prompt: '', schedule_type: 'manual',
+ schedule_value: '', enabled: true, agent_tools: 'Bash,Read,Write,Edit',
+ agent_system_prompt: '', agent_permission_mode: 'auto', agent_timeout: 300
+ });
+ const [editingTaskId, setEditingTaskId] = useState(null);
+ const [taskRuns, setTaskRuns] = useState({});
+
+ // MCP state
+ const [mcpServers, setMcpServers] = useState([]);
+ const [showMcpForm, setShowMcpForm] = useState(false);
+ const [mcpForm, setMcpForm] = useState({ name: '', server_type: 'sse', url: '', command: '' });
+
+ // ---- Data fetching ----
+ const fetchTasks = async () => {
+ try {
+ const r = await fetch(`${API}/api/tasks`);
+ if (r.ok) setTasks(await r.json());
+ } catch (e) { /* ignore */ }
+ };
+
+ const fetchMcpServers = async () => {
+ try {
+ const r = await fetch(`${API}/api/mcp/servers`);
+ if (r.ok) setMcpServers(await r.json());
+ } catch (e) { /* ignore */ }
+ };
+
+ const fetchChatSessions = async () => {
+ try {
+ const r = await fetch(`${API}/api/chat/sessions`);
+ if (r.ok) setChatSessions(await r.json());
+ } catch (e) { /* ignore */ }
+ };
+
+ const fetchClaudeStatus = async () => {
+ try {
+ const r = await fetch(`${API}/api/claude/status`);
+ if (r.ok) {
+ const d = await r.json();
+ setClaudeStatus(d.status);
+ setClaudeSessionId(d.session_id);
+ }
+ } catch (e) { /* ignore */ }
+ };
useEffect(() => {
- fetchTasks(); fetchSystemInfo(); fetchUsageStats(); fetchAuthStatus(); fetchMcpServers();
+ fetchTasks();
+ fetchMcpServers();
+ fetchChatSessions();
+ fetchClaudeStatus();
const interval = setInterval(() => {
- fetchTasks(); fetchSystemInfo(); fetchAuthStatus();
- }, 10000);
+ fetchTasks();
+ fetchClaudeStatus();
+ }, 8000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
- if (chatEndRef.current) chatEndRef.current.scrollIntoView({ behavior: 'smooth' });
+ if (chatEndRef.current) {
+ chatEndRef.current.scrollIntoView({ behavior: 'smooth' });
+ }
}, [chatMessages]);
- // WebSocket for chat
+ // ---- WebSocket ----
const connectWs = useCallback(() => {
- if (wsRef.current && wsRef.current.readyState <= 1) return;
+ if (wsRef.current && wsRef.current.readyState < 2) return; // already open or connecting
+ if (reconnectTimerRef.current) {
+ clearTimeout(reconnectTimerRef.current);
+ reconnectTimerRef.current = null;
+ }
+
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
- const ws = new WebSocket(`${proto}//${window.location.host}/api/chat/ws/${chatSessionId}`);
- ws.onopen = () => setWsConnected(true);
- ws.onclose = () => setWsConnected(false);
+ const ws = new WebSocket(`${proto}//${window.location.host}/api/chat/ws`);
+ wsRef.current = ws;
+
+ ws.onopen = () => {
+ setWsConnected(true);
+ setWsError(null);
+ };
+
+ ws.onclose = () => {
+ setWsConnected(false);
+ // Auto-reconnect after 3 seconds
+ reconnectTimerRef.current = setTimeout(connectWs, 3000);
+ };
+
+ ws.onerror = () => {
+ setWsError('WebSocket error — reconnecting...');
+ };
+
ws.onmessage = (event) => {
- const data = JSON.parse(event.data);
- if (data.type === 'chunk') {
- setChatMessages(prev => {
- const last = prev[prev.length - 1];
- if (last && last.role === 'assistant' && last.streaming) {
- return [...prev.slice(0, -1), { ...last, content: last.content + data.content }];
+ let data;
+ try { data = JSON.parse(event.data); } catch (e) { return; }
+
+ const type = data.type;
+
+ if (type === 'ping') return;
+
+ if (type === 'system') {
+ const sub = data.subtype;
+ if (sub === 'connected' || sub === 'status' || sub === 'start_result' || sub === 'restart_result') {
+ if (data.claude_status) setClaudeStatus(data.claude_status);
+ if (data.claude_session_id) setClaudeSessionId(data.claude_session_id);
+ }
+ if (sub === 'process_dead') {
+ setClaudeStatus('dead');
+ setChatWaiting(false);
+ appendSystemMsg('⚠️ Claude process stopped. Click "Start Claude" to restart.');
+ }
+ return;
+ }
+
+ if (type === 'user_ack') {
+ // We already added the user message optimistically, nothing to do
+ return;
+ }
+
+ if (type === 'error') {
+ setChatWaiting(false);
+ appendErrorMsg(data.message || 'Unknown error');
+ return;
+ }
+
+ // Claude stream-json events
+ if (type === 'assistant') {
+ // Full assistant message (not streaming)
+ const content = data.message?.content;
+ let text = '';
+ if (Array.isArray(content)) {
+ text = content.filter(b => b.type === 'text').map(b => b.text).join('');
+ } else if (typeof content === 'string') {
+ text = content;
+ }
+ if (text) {
+ pendingAssistantRef.current += text;
+ upsertStreamingMessage(pendingAssistantRef.current, false);
+ }
+ }
+
+ if (type === 'content_block_delta') {
+ const delta = data.delta;
+ if (delta?.type === 'text_delta') {
+ pendingAssistantRef.current += delta.text || '';
+ upsertStreamingMessage(pendingAssistantRef.current, true);
+ }
+ }
+
+ if (type === 'content_block_stop') {
+ // Block done, finalize
+ if (pendingAssistantRef.current) {
+ upsertStreamingMessage(pendingAssistantRef.current, false);
+ }
+ }
+
+ if (type === 'result') {
+ // Turn complete
+ const sub = data.subtype;
+ if (sub === 'success' || sub === 'error_max_turns') {
+ if (pendingAssistantRef.current) {
+ upsertStreamingMessage(pendingAssistantRef.current, false);
+ pendingAssistantRef.current = '';
}
- return [...prev, { role: 'assistant', content: data.content, streaming: true }];
- });
- } else if (data.type === 'done') {
- setChatMessages(prev => {
- const last = prev[prev.length - 1];
- if (last && last.role === 'assistant' && last.streaming) {
- return [...prev.slice(0, -1), { ...last, streaming: false }];
- }
- return prev;
- });
- setChatSending(false);
- } else if (data.type === 'error') {
- setChatMessages(prev => [...prev, { role: 'error', content: data.content }]);
- setChatSending(false);
- } else if (data.type === 'status' && data.content === 'thinking') {
- setChatMessages(prev => [...prev, { role: 'assistant', content: '', streaming: true }]);
+ if (data.session_id) setClaudeSessionId(data.session_id);
+ setChatWaiting(false);
+ fetchChatSessions(); // refresh session list
+ }
+ if (sub === 'error' || sub === 'error_during_execution') {
+ pendingAssistantRef.current = '';
+ setChatWaiting(false);
+ appendErrorMsg(data.error || 'Claude returned an error');
+ }
}
};
- wsRef.current = ws;
- }, [chatSessionId]);
+ }, []);
useEffect(() => {
- if (activeView === 'chat') connectWs();
+ connectWs();
return () => {
- if (wsRef.current) { wsRef.current.close(); wsRef.current = null; }
+ if (reconnectTimerRef.current) clearTimeout(reconnectTimerRef.current);
+ if (wsRef.current) wsRef.current.close();
};
- }, [activeView, chatSessionId, connectWs]);
+ }, [connectWs]);
- const sendChatMessage = async () => {
- if (!chatInput.trim() || chatSending) return;
- const msg = chatInput.trim();
+ const appendSystemMsg = (text) => {
+ setChatMessages(prev => [...prev, {
+ id: generateUUID(), role: 'system', content: text, timestamp: new Date().toISOString()
+ }]);
+ };
+
+ const appendErrorMsg = (text) => {
+ setChatMessages(prev => [...prev, {
+ id: generateUUID(), role: 'error', content: text, timestamp: new Date().toISOString()
+ }]);
+ };
+
+ const upsertStreamingMessage = (text, streaming) => {
+ setChatMessages(prev => {
+ const last = prev[prev.length - 1];
+ if (last && last.role === 'assistant' && (last.streaming || streaming)) {
+ return [...prev.slice(0, -1), { ...last, content: text, streaming }];
+ }
+ return [...prev, {
+ id: generateUUID(), role: 'assistant', content: text,
+ streaming, timestamp: new Date().toISOString()
+ }];
+ });
+ };
+
+ const sendMessage = () => {
+ const text = chatInput.trim();
+ if (!text || chatWaiting) return;
+
+ // Optimistic UI: add user message immediately
+ setChatMessages(prev => [...prev, {
+ id: generateUUID(), role: 'user', content: text, timestamp: new Date().toISOString()
+ }]);
setChatInput('');
- setChatMessages(prev => [...prev, { role: 'user', content: msg }]);
- setChatSending(true);
+ setChatWaiting(true);
+ pendingAssistantRef.current = '';
+ if (!wsRef.current || wsRef.current.readyState !== 1) {
+ appendErrorMsg('Not connected — reconnecting...');
+ setChatWaiting(false);
+ connectWs();
+ return;
+ }
+
+ wsRef.current.send(JSON.stringify({
+ action: 'send',
+ message: text,
+ session_id: chatSessionId
+ }));
+ };
+
+ const handleChatKeyDown = (e) => {
+ if (e.key === 'Enter' && !e.shiftKey) {
+ e.preventDefault();
+ sendMessage();
+ }
+ };
+
+ const startClaude = () => {
+ setClaudeStatus('starting');
if (wsRef.current && wsRef.current.readyState === 1) {
- wsRef.current.send(JSON.stringify({
- message: msg, model: chatModel || undefined
- }));
+ wsRef.current.send(JSON.stringify({ action: 'start_claude' }));
} else {
- // Fallback to HTTP
- try {
- const response = await fetch('/api/chat/send', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- message: msg, session_id: chatSessionId, model: chatModel || undefined
- })
+ fetch(`${API}/api/claude/start`, { method: 'POST' })
+ .then(r => r.json())
+ .then(d => {
+ setClaudeStatus(d.status);
+ if (d.session_id) setClaudeSessionId(d.session_id);
});
- const data = await response.json();
- setChatMessages(prev => [...prev, {
- role: data.status === 'ok' ? 'assistant' : 'error',
- content: data.response
- }]);
- } catch (error) {
- setChatMessages(prev => [...prev, { role: 'error', content: 'Failed to send message' }]);
- }
- setChatSending(false);
}
};
- const startNewChat = () => {
- setChatMessages([]);
- const newId = generateUUID();
- setChatSessionId(newId);
- if (wsRef.current) { wsRef.current.close(); wsRef.current = null; }
- };
-
- const loadChatSession = async (sid) => {
- setChatSessionId(sid);
- if (wsRef.current) { wsRef.current.close(); wsRef.current = null; }
- try {
- const response = await fetch(`/api/chat/history/${sid}`);
- const data = await response.json();
- setChatMessages(data.map(m => ({ role: m.role, content: m.content })));
- } catch (e) {
- console.error('Error loading chat session:', e);
+ const restartClaude = () => {
+ setClaudeStatus('starting');
+ if (wsRef.current && wsRef.current.readyState === 1) {
+ wsRef.current.send(JSON.stringify({ action: 'restart_claude' }));
+ } else {
+ fetch(`${API}/api/claude/restart`, { method: 'POST' })
+ .then(r => r.json())
+ .then(d => {
+ setClaudeStatus(d.status);
+ if (d.session_id) setClaudeSessionId(d.session_id);
+ });
}
};
- // Fetchers
- const fetchTasks = async () => { try { const r = await fetch('/api/tasks'); setTasks(await r.json()); } catch(e) {} };
- const fetchSystemInfo = async () => { try { const r = await fetch('/api/system/info'); setSystemInfo(await r.json()); } catch(e) {} };
- const fetchUsageStats = async () => { try { const r = await fetch('/api/system/usage'); setUsageStats(await r.json()); } catch(e) {} };
- const fetchAuthStatus = async () => { try { const r = await fetch('/api/auth/status'); setAuthStatus(await r.json()); } catch(e) {} };
- const fetchMcpServers = async () => { try { const r = await fetch('/api/mcp/servers'); setMcpServers(await r.json()); } catch(e) {} };
- const fetchChatSessions = async () => { try { const r = await fetch('/api/chat/sessions'); setChatSessions(await r.json()); } catch(e) {} };
-
- const handleSubmitToken = async (e) => {
- e.preventDefault();
- if (!authToken.trim()) return;
- setTokenSubmitting(true);
+ const loadHistorySession = async (sid) => {
+ setSelectedHistorySession(sid);
try {
- const r = await fetch('/api/auth/token', {
- method: 'POST', headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ token: authToken.trim(), token_type: tokenType })
- });
- const data = await r.json();
- setAuthToken('');
- fetchAuthStatus();
- if (data.status !== 'logged_in') alert(data.message);
- } catch(e) { alert('Failed to submit token'); }
- setTokenSubmitting(false);
- };
-
- const handleLogout = async () => { await fetch('/api/auth/logout', { method: 'POST' }); fetchAuthStatus(); };
-
- const handleAddMcp = async (e) => {
- e.preventDefault();
- const r = await fetch('/api/mcp/servers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(mcpForm) });
- const data = await r.json();
- if (data.status === 'ok') { setShowAddMcp(false); setMcpForm({ name: '', server_type: 'sse', url: '', command: '' }); fetchMcpServers(); }
- else alert(data.message || 'Failed');
- };
- const handleRemoveMcp = async (name) => { if (!confirm(`Remove "${name}"?`)) return; await fetch(`/api/mcp/servers/${name}`, { method: 'DELETE' }); fetchMcpServers(); };
-
- const handleCreateTask = async (e) => {
- e.preventDefault(); setLoading(true);
- try {
- const r = await fetch('/api/tasks', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(formData) });
+ const r = await fetch(`${API}/api/chat/sessions/${sid}/messages`);
if (r.ok) {
- await fetchTasks(); setShowForm(false);
- setFormData({ name: '', description: '', prompt: '', schedule_type: 'manual', schedule_value: '', enabled: true,
- agent_model: '', agent_tools: 'Bash,Read,Write,Edit', agent_system_prompt: '', agent_permission_mode: 'auto', agent_timeout: 300 });
+ const msgs = await r.json();
+ setChatMessages(msgs.map(m => ({
+ id: m.id, role: m.role, content: m.content, timestamp: m.timestamp, streaming: false
+ })));
}
- } catch(e) {}
- setLoading(false);
+ } catch (e) { /* ignore */ }
};
- const handleRunTask = async (taskId) => { await fetch(`/api/tasks/${taskId}/run`, { method: 'POST' }); await fetchTasks(); };
- const handleDeleteTask = async (taskId) => {
- if (confirm('Delete this task?')) { await fetch(`/api/tasks/${taskId}`, { method: 'DELETE' }); await fetchTasks(); setSelectedTask(null); }
+ // ---- Tasks ----
+ const saveTask = async () => {
+ const method = editingTaskId ? 'PUT' : 'POST';
+ const url = editingTaskId ? `${API}/api/tasks/${editingTaskId}` : `${API}/api/tasks`;
+ try {
+ const r = await fetch(url, {
+ method,
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(taskForm)
+ });
+ if (r.ok) {
+ fetchTasks();
+ setShowTaskForm(false);
+ setEditingTaskId(null);
+ setTaskForm({
+ name: '', description: '', prompt: '', schedule_type: 'manual',
+ schedule_value: '', enabled: true, agent_tools: 'Bash,Read,Write,Edit',
+ agent_system_prompt: '', agent_permission_mode: 'auto', agent_timeout: 300
+ });
+ }
+ } catch (e) { alert('Failed to save task'); }
};
- const getStatusColor = (s) => ({ running: '#3498db', completed: '#27ae60', failed: '#e74c3c' }[s] || '#95a5a6');
+ const deleteTask = async (id) => {
+ if (!window.confirm('Delete this task?')) return;
+ await fetch(`${API}/api/tasks/${id}`, { method: 'DELETE' });
+ fetchTasks();
+ };
+ const runTask = async (id) => {
+ try {
+ const r = await fetch(`${API}/api/tasks/${id}/run`, { method: 'POST' });
+ if (r.ok) {
+ alert('Task started!');
+ fetchTasks();
+ }
+ } catch (e) { alert('Failed to run task'); }
+ };
+
+ const editTask = (task) => {
+ setEditingTaskId(task.id);
+ setTaskForm({ ...task });
+ setShowTaskForm(true);
+ };
+
+ const fetchTaskRuns = async (taskId) => {
+ try {
+ const r = await fetch(`${API}/api/tasks/${taskId}/runs`);
+ if (r.ok) {
+ setTaskRuns(prev => ({ ...prev, [taskId]: await r.json() }));
+ }
+ } catch (e) { /* ignore */ }
+ };
+
+ // ---- MCP ----
+ const saveMcp = async () => {
+ try {
+ const r = await fetch(`${API}/api/mcp/servers`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(mcpForm)
+ });
+ if (r.ok) {
+ fetchMcpServers();
+ setShowMcpForm(false);
+ setMcpForm({ name: '', server_type: 'sse', url: '', command: '' });
+ }
+ } catch (e) { alert('Failed to add MCP server'); }
+ };
+
+ const removeMcp = async (name) => {
+ if (!window.confirm(`Remove MCP server "${name}"?`)) return;
+ await fetch(`${API}/api/mcp/servers/${name}`, { method: 'DELETE' });
+ fetchMcpServers();
+ };
+
+ // ---- Status helpers ----
+ const claudeStatusColor = {
+ ready: '#22c55e',
+ starting: '#f59e0b',
+ dead: '#ef4444',
+ not_started: '#6b7280',
+ unknown: '#6b7280'
+ }[claudeStatus] || '#6b7280';
+
+ const claudeStatusLabel = {
+ ready: 'Running',
+ starting: 'Starting…',
+ dead: 'Stopped',
+ not_started: 'Not Started',
+ unknown: 'Unknown'
+ }[claudeStatus] || claudeStatus;
+
+ // ---- Render ----
return (
-