import React, { useState, useEffect, useRef, useCallback } from 'react';
import './App.css';
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'
// Auth
const [authToken, setAuthToken] = useState('');
const [tokenType, setTokenType] = useState('oauth_token');
const [tokenSubmitting, setTokenSubmitting] = useState(false);
// Chat
const [chatMessages, setChatMessages] = useState([]);
const [chatInput, setChatInput] = useState('');
const [chatSending, setChatSending] = useState(false);
const [chatSessionId, setChatSessionId] = useState(() => crypto.randomUUID ? crypto.randomUUID() : Date.now().toString());
const [chatSessions, setChatSessions] = useState([]);
const [chatModel, setChatModel] = useState('');
const chatEndRef = useRef(null);
const wsRef = useRef(null);
const [wsConnected, setWsConnected] = useState(false);
useEffect(() => {
fetchTasks(); fetchSystemInfo(); fetchUsageStats(); fetchAuthStatus(); fetchMcpServers();
const interval = setInterval(() => {
fetchTasks(); fetchSystemInfo(); fetchAuthStatus();
}, 10000);
return () => clearInterval(interval);
}, []);
useEffect(() => {
if (chatEndRef.current) chatEndRef.current.scrollIntoView({ behavior: 'smooth' });
}, [chatMessages]);
// WebSocket for chat
const connectWs = useCallback(() => {
if (wsRef.current && wsRef.current.readyState <= 1) return;
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);
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 }];
}
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 }]);
}
};
wsRef.current = ws;
}, [chatSessionId]);
useEffect(() => {
if (activeView === 'chat') connectWs();
return () => {
if (wsRef.current) { wsRef.current.close(); wsRef.current = null; }
};
}, [activeView, chatSessionId, connectWs]);
const sendChatMessage = async () => {
if (!chatInput.trim() || chatSending) return;
const msg = chatInput.trim();
setChatInput('');
setChatMessages(prev => [...prev, { role: 'user', content: msg }]);
setChatSending(true);
if (wsRef.current && wsRef.current.readyState === 1) {
wsRef.current.send(JSON.stringify({
message: msg, model: chatModel || undefined
}));
} 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
})
});
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 = crypto.randomUUID ? crypto.randomUUID() : Date.now().toString();
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);
}
};
// 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);
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) });
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 });
}
} catch(e) {}
setLoading(false);
};
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); }
};
const getStatusColor = (s) => ({ running: '#3498db', completed: '#27ae60', failed: '#e74c3c' }[s] || '#95a5a6');
return (
Claude Persistent Agent
Chat, schedule tasks & orchestrate agents
{authStatus?.status === 'logged_in' ? (
● {authStatus.account || 'Authenticated'}
) : authStatus?.has_saved_token ? (
● Token saved
) : (
setActiveView('dashboard')} style={{cursor:'pointer'}}>⚠ Not authenticated
)}
{systemInfo && (
{systemInfo.task_count} agents · {systemInfo.total_runs || 0} runs
)}
{/* ===== CHAT VIEW ===== */}
{activeView === 'chat' && (
{chatSessions.map(s => (
loadChatSession(s.session_id)}>
{(s.first_message || 'Chat').substring(0, 40)}
{s.message_count} msgs · {new Date(s.last_message).toLocaleDateString()}
))}
{wsConnected ? 'Connected' : 'Disconnected'}
{chatMessages.length === 0 && (
💬
Chat with Claude
Send a message to start a conversation with the Claude Code instance running on your server.
Claude has access to Bash, file system, and any MCP servers you've configured.
)}
{chatMessages.map((msg, i) => (
{msg.role === 'user' ? '👤' : msg.role === 'error' ? '⚠️' : '🤖'}
{msg.content}{msg.streaming ? '▊' : ''}
))}
)}
{/* ===== TASKS/AGENTS VIEW ===== */}
{activeView === 'tasks' && (<>
{showForm ? (
) : selectedTask ? (
{selectedTask.name}
Status{selectedTask.status}
Schedule{selectedTask.schedule_type === 'manual' ? 'Manual' : `${selectedTask.schedule_type}: ${selectedTask.schedule_value}`}
Model{selectedTask.agent_model || 'Default'}
Tools{selectedTask.agent_tools || 'Bash,Read,Write,Edit'}
Permission{selectedTask.agent_permission_mode || 'auto'}
Timeout{selectedTask.agent_timeout || 300}s
Created{new Date(selectedTask.created_at).toLocaleString()}
{selectedTask.last_run &&
Last Run{new Date(selectedTask.last_run).toLocaleString()}
}
{selectedTask.description &&
Description
{selectedTask.description}
}
{selectedTask.agent_system_prompt &&
System Prompt
{selectedTask.agent_system_prompt}}
Agent Prompt
{selectedTask.prompt}
) : (
Select an agent task to view details
Or create a new one to get started
)}
>)}
{/* ===== DASHBOARD VIEW ===== */}
{activeView === 'dashboard' && (
System Dashboard
{systemInfo && (<>
📋
{systemInfo.task_count}
Agent Tasks
✅
{systemInfo.completed_runs || 0}
Completed
❌
{systemInfo.failed_runs || 0}
Failed
⚡
{systemInfo.running_runs || 0}
Running
💬
{systemInfo.active_chat_sessions || 0}
Active Chats
{systemInfo.scheduler_running ? '🟢' : '🔴'}
{systemInfo.scheduler_running ? 'Active' : 'Stopped'}
Scheduler
>)}
{/* Auth */}
Claude Authentication
{authStatus?.status === 'logged_in' ? (
✅
Connected to Claude
{authStatus.account || 'Authenticated'}{authStatus.auth_method && ` (${authStatus.auth_method})`}
) : (
🔐
{authStatus?.has_saved_token ? 'Update Token' : 'Authenticate'}
Two options:
Option 1: Setup Token (Claude Max/Pro)
Run on TrueNAS shell:
docker exec -it claude-persistent-agent claude setup-token
Paste the sk-ant-oat01-... token below.
{authStatus?.has_saved_token && (
Saved: {authStatus.token_type === 'api_key' ? 'API Key' : 'OAuth Token'}
)}
)}
{/* MCP */}
MCP Servers
{mcpServers?.servers?.length > 0 ? (
{mcpServers.servers.map((srv, i) => (
{srv.name || srv}
{srv.details || srv.url || ''}
))}
) :
No MCP servers configured.
}
{showAddMcp ? (
) :
}
{/* Usage */}
Usage
{usageStats ? (<>
Total Runs{usageStats.claude_runs_total ?? '—'}
Sessions{usageStats.session_count ?? '—'}
Next Reset{usageStats.next_reset ? new Date(usageStats.next_reset).toLocaleDateString() : '—'}
Days Until Reset{usageStats.days_until_reset ?? '—'}
>) :
Loading...
}
)}
);
};
const TaskRuns = ({ taskId }) => {
const [runs, setRuns] = useState([]);
useEffect(() => {
const fetch_ = async () => { try { const r = await fetch(`/api/tasks/${taskId}/runs`); setRuns(await r.json()); } catch(e) {} };
fetch_();
const i = setInterval(fetch_, 3000);
return () => clearInterval(i);
}, [taskId]);
return (
Run History
{runs.length === 0 ?
No runs yet
: (
{runs.map(run => (
{run.status}
{new Date(run.started_at).toLocaleString()}
{run.output &&
Output
{run.output}}
{run.error &&
Error
{run.error}}
))}
)}
);
};
export default App;