2026-04-05 00:26:21 -04:00
import React , { useState , useEffect , useRef , useCallback } from 'react' ;
2026-04-04 22:40:51 -04:00
import './App.css' ;
const App = ( ) => {
const [ tasks , setTasks ] = useState ( [ ] ) ;
const [ selectedTask , setSelectedTask ] = useState ( null ) ;
const [ showForm , setShowForm ] = useState ( false ) ;
const [ formData , setFormData ] = useState ( {
2026-04-05 00:26:21 -04:00
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
2026-04-04 22:40:51 -04:00
} ) ;
const [ systemInfo , setSystemInfo ] = useState ( null ) ;
2026-04-04 22:46:16 -04:00
const [ usageStats , setUsageStats ] = useState ( null ) ;
2026-04-04 22:51:39 -04:00
const [ authStatus , setAuthStatus ] = useState ( null ) ;
2026-04-04 23:50:16 -04:00
const [ mcpServers , setMcpServers ] = useState ( null ) ;
const [ showAddMcp , setShowAddMcp ] = useState ( false ) ;
const [ mcpForm , setMcpForm ] = useState ( { name : '' , server _type : 'sse' , url : '' , command : '' } ) ;
2026-04-04 22:40:51 -04:00
const [ loading , setLoading ] = useState ( false ) ;
2026-04-05 00:26:21 -04:00
const [ activeView , setActiveView ] = useState ( 'chat' ) ; // 'chat' | 'tasks' | 'dashboard'
2026-04-05 00:13:37 -04:00
2026-04-05 00:26:21 -04:00
// Auth
2026-04-05 00:13:37 -04:00
const [ authToken , setAuthToken ] = useState ( '' ) ;
const [ tokenType , setTokenType ] = useState ( 'oauth_token' ) ;
const [ tokenSubmitting , setTokenSubmitting ] = useState ( false ) ;
2026-04-04 22:40:51 -04:00
2026-04-05 00:26:21 -04:00
// 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 ) ;
2026-04-04 22:40:51 -04:00
useEffect ( ( ) => {
2026-04-05 00:26:21 -04:00
fetchTasks ( ) ; fetchSystemInfo ( ) ; fetchUsageStats ( ) ; fetchAuthStatus ( ) ; fetchMcpServers ( ) ;
2026-04-04 22:40:51 -04:00
const interval = setInterval ( ( ) => {
2026-04-05 00:26:21 -04:00
fetchTasks ( ) ; fetchSystemInfo ( ) ; fetchAuthStatus ( ) ;
2026-04-04 22:46:16 -04:00
} , 10000 ) ;
2026-04-04 22:40:51 -04:00
return ( ) => clearInterval ( interval ) ;
} , [ ] ) ;
2026-04-05 00:26:21 -04:00
useEffect ( ( ) => {
if ( chatEndRef . current ) chatEndRef . current . scrollIntoView ( { behavior : 'smooth' } ) ;
} , [ chatMessages ] ) ;
2026-04-04 22:40:51 -04:00
2026-04-05 00:26:21 -04:00
// 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 ) ;
2026-04-04 22:40:51 -04:00
}
} ;
2026-04-05 00:26:21 -04:00
const startNewChat = ( ) => {
setChatMessages ( [ ] ) ;
const newId = crypto . randomUUID ? crypto . randomUUID ( ) : Date . now ( ) . toString ( ) ;
setChatSessionId ( newId ) ;
if ( wsRef . current ) { wsRef . current . close ( ) ; wsRef . current = null ; }
2026-04-04 22:46:16 -04:00
} ;
2026-04-05 00:26:21 -04:00
const loadChatSession = async ( sid ) => {
setChatSessionId ( sid ) ;
if ( wsRef . current ) { wsRef . current . close ( ) ; wsRef . current = null ; }
2026-04-04 22:51:39 -04:00
try {
2026-04-05 00:26:21 -04:00
const response = await fetch ( ` /api/chat/history/ ${ sid } ` ) ;
2026-04-04 22:51:39 -04:00
const data = await response . json ( ) ;
2026-04-05 00:26:21 -04:00
setChatMessages ( data . map ( m => ( { role : m . role , content : m . content } ) ) ) ;
} catch ( e ) {
console . error ( 'Error loading chat session:' , e ) ;
2026-04-04 22:51:39 -04:00
}
} ;
2026-04-05 00:26:21 -04:00
// 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 ) { } } ;
2026-04-05 00:13:37 -04:00
const handleSubmitToken = async ( e ) => {
2026-04-04 23:54:24 -04:00
e . preventDefault ( ) ;
2026-04-05 00:13:37 -04:00
if ( ! authToken . trim ( ) ) return ;
setTokenSubmitting ( true ) ;
2026-04-04 23:54:24 -04:00
try {
2026-04-05 00:26:21 -04:00
const r = await fetch ( '/api/auth/token' , {
method : 'POST' , headers : { 'Content-Type' : 'application/json' } ,
2026-04-05 00:13:37 -04:00
body : JSON . stringify ( { token : authToken . trim ( ) , token _type : tokenType } )
2026-04-04 23:54:24 -04:00
} ) ;
2026-04-05 00:26:21 -04:00
const data = await r . json ( ) ;
setAuthToken ( '' ) ;
2026-04-04 22:51:39 -04:00
fetchAuthStatus ( ) ;
2026-04-05 00:26:21 -04:00
if ( data . status !== 'logged_in' ) alert ( data . message ) ;
} catch ( e ) { alert ( 'Failed to submit token' ) ; }
setTokenSubmitting ( false ) ;
2026-04-04 22:51:39 -04:00
} ;
2026-04-05 00:26:21 -04:00
const handleLogout = async ( ) => { await fetch ( '/api/auth/logout' , { method : 'POST' } ) ; fetchAuthStatus ( ) ; } ;
2026-04-04 23:50:16 -04:00
const handleAddMcp = async ( e ) => {
e . preventDefault ( ) ;
2026-04-05 00:26:21 -04:00
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' ) ;
2026-04-04 23:50:16 -04:00
} ;
2026-04-05 00:26:21 -04:00
const handleRemoveMcp = async ( name ) => { if ( ! confirm ( ` Remove " ${ name } "? ` ) ) return ; await fetch ( ` /api/mcp/servers/ ${ name } ` , { method : 'DELETE' } ) ; fetchMcpServers ( ) ; } ;
2026-04-04 23:50:16 -04:00
2026-04-04 22:40:51 -04:00
const handleCreateTask = async ( e ) => {
2026-04-05 00:26:21 -04:00
e . preventDefault ( ) ; setLoading ( true ) ;
2026-04-04 22:40:51 -04:00
try {
2026-04-05 00:26:21 -04:00
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 } ) ;
2026-04-04 22:40:51 -04:00
}
2026-04-05 00:26:21 -04:00
} catch ( e ) { }
2026-04-04 22:40:51 -04:00
setLoading ( false ) ;
} ;
2026-04-05 00:26:21 -04:00
const handleRunTask = async ( taskId ) => { await fetch ( ` /api/tasks/ ${ taskId } /run ` , { method : 'POST' } ) ; await fetchTasks ( ) ; } ;
2026-04-04 22:40:51 -04:00
const handleDeleteTask = async ( taskId ) => {
2026-04-05 00:26:21 -04:00
if ( confirm ( 'Delete this task?' ) ) { await fetch ( ` /api/tasks/ ${ taskId } ` , { method : 'DELETE' } ) ; await fetchTasks ( ) ; setSelectedTask ( null ) ; }
2026-04-04 22:40:51 -04:00
} ;
2026-04-05 00:26:21 -04:00
const getStatusColor = ( s ) => ( { running : '#3498db' , completed : '#27ae60' , failed : '#e74c3c' } [ s ] || '#95a5a6' ) ;
2026-04-04 22:40:51 -04:00
return (
< div className = "app-container" >
< header className = "app-header" >
< div className = "header-content" >
< h1 > Claude Persistent Agent < / h1 >
2026-04-05 00:26:21 -04:00
< p > Chat , schedule tasks & orchestrate agents < / p >
2026-04-04 22:40:51 -04:00
< / div >
2026-04-04 22:46:16 -04:00
< div className = "header-right" >
< nav className = "header-nav" >
2026-04-05 00:26:21 -04:00
< button className = { ` nav-btn ${ activeView === 'chat' ? 'active' : '' } ` } onClick = { ( ) => setActiveView ( 'chat' ) } > Chat < / button >
< button className = { ` nav-btn ${ activeView === 'tasks' ? 'active' : '' } ` } onClick = { ( ) => setActiveView ( 'tasks' ) } > Agents < / button >
2026-04-04 22:46:16 -04:00
< button className = { ` nav-btn ${ activeView === 'dashboard' ? 'active' : '' } ` } onClick = { ( ) => setActiveView ( 'dashboard' ) } > Dashboard < / button >
< / nav >
2026-04-04 22:51:39 -04:00
< div className = "auth-badge" >
{ authStatus ? . status === 'logged_in' ? (
2026-04-05 00:26:21 -04:00
< span className = "auth-ok" title = { authStatus . account || 'Authenticated' } > ● { authStatus . account || 'Authenticated' } < / span >
2026-04-05 00:13:37 -04:00
) : authStatus ? . has _saved _token ? (
2026-04-05 00:26:21 -04:00
< span className = "auth-ok auth-token-saved" title = "Token saved" > ● Token saved < / span >
2026-04-04 22:51:39 -04:00
) : (
2026-04-05 00:26:21 -04:00
< span className = "auth-disconnected-badge" onClick = { ( ) => setActiveView ( 'dashboard' ) } style = { { cursor : 'pointer' } } > ⚠ Not authenticated < / span >
2026-04-04 22:51:39 -04:00
) }
< / div >
2026-04-04 22:46:16 -04:00
{ systemInfo && (
< div className = "system-status" >
< span className = { ` status-indicator ${ systemInfo . scheduler _running ? 'running' : 'stopped' } ` } > < / span >
2026-04-05 00:26:21 -04:00
< span > { systemInfo . task _count } agents · { systemInfo . total _runs || 0 } runs < / span >
2026-04-04 22:46:16 -04:00
< / div >
) }
< / div >
2026-04-04 22:40:51 -04:00
< / header >
< main className = "app-main" >
2026-04-05 00:13:37 -04:00
2026-04-05 00:26:21 -04:00
{ /* ===== CHAT VIEW ===== */ }
{ activeView === 'chat' && (
< div className = "chat-view" >
< div className = "chat-sidebar" >
< button className = "btn btn-primary btn-sm chat-new-btn" onClick = { startNewChat } > + New Chat < / button >
< div className = "chat-model-select" >
< select value = { chatModel } onChange = { e => setChatModel ( e . target . value ) } >
< option value = "" > Default Model < / option >
< option value = "sonnet" > Sonnet < / option >
< option value = "opus" > Opus < / option >
< option value = "haiku" > Haiku < / option >
< / select >
< / div >
< div className = "chat-sessions-list" >
< button className = "btn btn-secondary btn-xs" onClick = { fetchChatSessions } style = { { width : '100%' , marginBottom : '0.5rem' } } > Load History < / button >
{ chatSessions . map ( s => (
< div key = { s . session _id } className = { ` chat-session-item ${ s . session _id === chatSessionId ? 'active' : '' } ` }
onClick = { ( ) => loadChatSession ( s . session _id ) } >
< div className = "chat-session-preview" > { ( s . first _message || 'Chat' ) . substring ( 0 , 40 ) } < / div >
< div className = "chat-session-meta" > { s . message _count } msgs · { new Date ( s . last _message ) . toLocaleDateString ( ) } < / div >
2026-04-04 22:51:39 -04:00
< / div >
2026-04-05 00:26:21 -04:00
) ) }
< / div >
< div className = "chat-ws-status" >
< span className = { ` ws-dot ${ wsConnected ? 'connected' : 'disconnected' } ` } > < / span >
{ wsConnected ? 'Connected' : 'Disconnected' }
2026-04-04 22:51:39 -04:00
< / div >
< / div >
2026-04-05 00:26:21 -04:00
< div className = "chat-main" >
< div className = "chat-messages" >
{ chatMessages . length === 0 && (
< div className = "chat-empty" >
< div className = "chat-empty-icon" > 💬 < / div >
< h3 > Chat with Claude < / h3 >
< p > Send a message to start a conversation with the Claude Code instance running on your server . < / p >
< p className = "chat-empty-hint" > Claude has access to Bash , file system , and any MCP servers you ' ve configured . < / p >
2026-04-04 23:50:16 -04:00
< / div >
) }
2026-04-05 00:26:21 -04:00
{ chatMessages . map ( ( msg , i ) => (
< div key = { i } className = { ` chat-msg chat-msg- ${ msg . role } ` } >
< div className = "chat-msg-avatar" >
{ msg . role === 'user' ? '👤' : msg . role === 'error' ? '⚠️' : '🤖' }
2026-04-04 23:50:16 -04:00
< / div >
2026-04-05 00:26:21 -04:00
< div className = "chat-msg-content" >
< pre className = "chat-msg-text" > { msg . content } { msg . streaming ? '▊' : '' } < / pre >
2026-04-04 23:50:16 -04:00
< / div >
2026-04-05 00:26:21 -04:00
< / div >
) ) }
< div ref = { chatEndRef } / >
2026-04-04 22:46:16 -04:00
< / div >
2026-04-05 00:26:21 -04:00
< div className = "chat-input-area" >
< textarea
className = "chat-input"
value = { chatInput }
onChange = { e => setChatInput ( e . target . value ) }
onKeyDown = { e => { if ( e . key === 'Enter' && ! e . shiftKey ) { e . preventDefault ( ) ; sendChatMessage ( ) ; } } }
placeholder = { chatSending ? 'Claude is thinking...' : 'Send a message... (Enter to send, Shift+Enter for newline)' }
disabled = { chatSending }
rows = { 2 }
/ >
< button className = "chat-send-btn" onClick = { sendChatMessage } disabled = { chatSending || ! chatInput . trim ( ) } >
{ chatSending ? '⏳' : '→' }
< / button >
2026-04-04 22:46:16 -04:00
< / div >
< / div >
< / div >
) }
2026-04-05 00:26:21 -04:00
{ /* ===== TASKS/AGENTS VIEW ===== */ }
2026-04-04 22:47:31 -04:00
{ activeView === 'tasks' && ( < >
2026-04-04 22:40:51 -04:00
< aside className = "sidebar" >
2026-04-05 00:26:21 -04:00
< button className = "btn btn-primary btn-create" onClick = { ( ) => setShowForm ( true ) } > + New Agent Task < / button >
2026-04-04 22:40:51 -04:00
< div className = "tasks-list" >
2026-04-05 00:26:21 -04:00
< h2 > Agent Tasks < / h2 >
2026-04-04 22:40:51 -04:00
{ tasks . length === 0 ? (
2026-04-05 00:26:21 -04:00
< p className = "empty-state" > No agent tasks yet . Create one to get started . < / p >
2026-04-04 22:40:51 -04:00
) : (
tasks . map ( ( task ) => (
2026-04-05 00:13:37 -04:00
< div key = { task . id } className = { ` task-item ${ selectedTask ? . id === task . id ? 'active' : '' } ` } onClick = { ( ) => setSelectedTask ( task ) } >
2026-04-04 22:40:51 -04:00
< div className = "task-item-header" >
< h3 > { task . name } < / h3 >
2026-04-05 00:13:37 -04:00
< span className = "status-badge" style = { { backgroundColor : getStatusColor ( task . status ) } } > { task . status } < / span >
2026-04-04 22:40:51 -04:00
< / div >
2026-04-05 00:26:21 -04:00
< p className = "task-schedule" >
{ task . schedule _type === 'manual' ? '⚡ Manual' : ` 🔄 ${ task . schedule _type } : ${ task . schedule _value } ` }
{ task . agent _model && ` · ${ task . agent _model } ` }
< / p >
{ task . last _run && < p className = "task-last-run" > Last : { new Date ( task . last _run ) . toLocaleString ( ) } < / p > }
2026-04-04 22:40:51 -04:00
< / div >
) )
) }
< / div >
< / aside >
< section className = "content" >
{ showForm ? (
< div className = "form-container" >
2026-04-05 00:26:21 -04:00
< h2 > Create Agent Task < / h2 >
2026-04-04 22:40:51 -04:00
< form onSubmit = { handleCreateTask } className = "task-form" >
< div className = "form-group" >
< label > Task Name < / label >
2026-04-05 00:26:21 -04:00
< input type = "text" required value = { formData . name } onChange = { e => setFormData ( { ... formData , name : e . target . value } ) } placeholder = "e.g., Daily Code Review" / >
2026-04-04 22:40:51 -04:00
< / div >
< div className = "form-group" >
< label > Description < / label >
2026-04-05 00:26:21 -04:00
< textarea value = { formData . description } onChange = { e => setFormData ( { ... formData , description : e . target . value } ) } placeholder = "What does this agent do?" rows = { 2 } / >
2026-04-04 22:40:51 -04:00
< / div >
< div className = "form-group" >
2026-04-05 00:26:21 -04:00
< label > Agent Prompt < / label >
< textarea required value = { formData . prompt } onChange = { e => setFormData ( { ... formData , prompt : e . target . value } ) } placeholder = "Instructions for the Claude agent..." rows = { 6 } / >
2026-04-04 22:40:51 -04:00
< / div >
2026-04-05 00:26:21 -04:00
< div className = "form-section-title" > Schedule < / div >
2026-04-04 22:40:51 -04:00
< div className = "form-row" >
< div className = "form-group" >
< label > Schedule Type < / label >
2026-04-05 00:26:21 -04:00
< select value = { formData . schedule _type } onChange = { e => setFormData ( { ... formData , schedule _type : e . target . value } ) } >
2026-04-04 22:40:51 -04:00
< option value = "manual" > Manual Only < / option >
< option value = "recurring" > Recurring ( Cron ) < / option >
< option value = "once" > Once ( Datetime ) < / option >
< / select >
< / div >
{ formData . schedule _type === 'recurring' && (
< div className = "form-group" >
< label > Cron Expression < / label >
2026-04-05 00:26:21 -04:00
< input type = "text" value = { formData . schedule _value } onChange = { e => setFormData ( { ... formData , schedule _value : e . target . value } ) } placeholder = "0 9 * * *" / >
2026-04-04 22:40:51 -04:00
< / div >
) }
{ formData . schedule _type === 'once' && (
2026-04-05 00:26:21 -04:00
< div className = "form-group" > < label > Run At < / label > < input type = "datetime-local" value = { formData . schedule _value } onChange = { e => setFormData ( { ... formData , schedule _value : e . target . value } ) } / > < / div >
2026-04-04 22:40:51 -04:00
) }
< / div >
2026-04-05 00:26:21 -04:00
< div className = "form-section-title" > Agent Configuration < / div >
< div className = "form-row" >
< div className = "form-group" >
< label > Model < / label >
< select value = { formData . agent _model } onChange = { e => setFormData ( { ... formData , agent _model : e . target . value } ) } >
< option value = "" > Default < / option >
< option value = "sonnet" > Sonnet ( fast ) < / option >
< option value = "opus" > Opus ( powerful ) < / option >
< option value = "haiku" > Haiku ( lightweight ) < / option >
< / select >
< / div >
< div className = "form-group" >
< label > Permission Mode < / label >
< select value = { formData . agent _permission _mode } onChange = { e => setFormData ( { ... formData , agent _permission _mode : e . target . value } ) } >
< option value = "auto" > Auto ( full access ) < / option >
< option value = "acceptEdits" > Accept Edits Only < / option >
< option value = "plan" > Plan Mode ( read - only ) < / option >
< / select >
< / div >
< / div >
< div className = "form-row" >
< div className = "form-group" >
< label > Allowed Tools < / label >
< input type = "text" value = { formData . agent _tools } onChange = { e => setFormData ( { ... formData , agent _tools : e . target . value } ) } placeholder = "Bash,Read,Write,Edit" / >
< / div >
< div className = "form-group" >
< label > Timeout ( seconds ) < / label >
< input type = "number" value = { formData . agent _timeout } onChange = { e => setFormData ( { ... formData , agent _timeout : parseInt ( e . target . value ) || 300 } ) } min = { 30 } max = { 3600 } / >
< / div >
< / div >
< div className = "form-group" >
< label > Custom System Prompt ( optional ) < / label >
< textarea value = { formData . agent _system _prompt } onChange = { e => setFormData ( { ... formData , agent _system _prompt : e . target . value } ) } placeholder = "Override the default system prompt..." rows = { 3 } / >
< / div >
2026-04-04 22:40:51 -04:00
< div className = "form-group checkbox" >
2026-04-05 00:26:21 -04:00
< input type = "checkbox" checked = { formData . enabled } onChange = { e => setFormData ( { ... formData , enabled : e . target . checked } ) } / >
2026-04-04 22:40:51 -04:00
< label > Enabled < / label >
< / div >
< div className = "form-actions" >
2026-04-05 00:26:21 -04:00
< button type = "submit" className = "btn btn-primary" disabled = { loading } > { loading ? 'Creating...' : 'Create Agent Task' } < / button >
2026-04-05 00:13:37 -04:00
< button type = "button" className = "btn btn-secondary" onClick = { ( ) => setShowForm ( false ) } > Cancel < / button >
2026-04-04 22:40:51 -04:00
< / div >
< / form >
< / div >
) : selectedTask ? (
< div className = "task-detail" >
< div className = "task-detail-header" >
< h2 > { selectedTask . name } < / h2 >
< div className = "task-actions" >
2026-04-05 00:13:37 -04:00
< button className = "btn btn-success" onClick = { ( ) => handleRunTask ( selectedTask . id ) } > ▶ Run Now < / button >
< button className = "btn btn-danger" onClick = { ( ) => handleDeleteTask ( selectedTask . id ) } > 🗑 Delete < / button >
2026-04-04 22:40:51 -04:00
< / div >
< / div >
< div className = "task-meta" >
2026-04-05 00:26:21 -04:00
< div className = "meta-item" > < span className = "label" > Status < / span > < span className = "value" style = { { color : getStatusColor ( selectedTask . status ) } } > { selectedTask . status } < / span > < / div >
< div className = "meta-item" > < span className = "label" > Schedule < / span > < span className = "value" > { selectedTask . schedule _type === 'manual' ? 'Manual' : ` ${ selectedTask . schedule _type } : ${ selectedTask . schedule _value } ` } < / span > < / div >
< div className = "meta-item" > < span className = "label" > Model < / span > < span className = "value" > { selectedTask . agent _model || 'Default' } < / span > < / div >
< div className = "meta-item" > < span className = "label" > Tools < / span > < span className = "value" > { selectedTask . agent _tools || 'Bash,Read,Write,Edit' } < / span > < / div >
< div className = "meta-item" > < span className = "label" > Permission < / span > < span className = "value" > { selectedTask . agent _permission _mode || 'auto' } < / span > < / div >
< div className = "meta-item" > < span className = "label" > Timeout < / span > < span className = "value" > { selectedTask . agent _timeout || 300 } s < / span > < / div >
< div className = "meta-item" > < span className = "label" > Created < / span > < span className = "value" > { new Date ( selectedTask . created _at ) . toLocaleString ( ) } < / span > < / div >
{ selectedTask . last _run && < div className = "meta-item" > < span className = "label" > Last Run < / span > < span className = "value" > { new Date ( selectedTask . last _run ) . toLocaleString ( ) } < / span > < / div > }
2026-04-04 22:40:51 -04:00
< / div >
2026-04-05 00:13:37 -04:00
{ selectedTask . description && < div className = "task-section" > < h3 > Description < / h3 > < p > { selectedTask . description } < / p > < / div > }
2026-04-05 00:26:21 -04:00
{ selectedTask . agent _system _prompt && < div className = "task-section" > < h3 > System Prompt < / h3 > < pre className = "prompt-display" > { selectedTask . agent _system _prompt } < / pre > < / div > }
< div className = "task-section" > < h3 > Agent Prompt < / h3 > < pre className = "prompt-display" > { selectedTask . prompt } < / pre > < / div >
2026-04-04 22:40:51 -04:00
< TaskRuns taskId = { selectedTask . id } / >
< / div >
) : (
< div className = "empty-state-main" >
2026-04-05 00:26:21 -04:00
< h2 > Select an agent task to view details < / h2 >
2026-04-04 22:40:51 -04:00
< p > Or create a new one to get started < / p >
< / div >
) }
< / section >
2026-04-05 00:13:37 -04:00
< / > ) }
2026-04-05 00:26:21 -04:00
{ /* ===== DASHBOARD VIEW ===== */ }
{ activeView === 'dashboard' && (
< div className = "dashboard-view" >
< h2 className = "dashboard-title" > System Dashboard < / h2 >
< div className = "dashboard-grid" >
{ systemInfo && ( < >
< div className = "dash-card" > < div className = "dash-card-icon" > 📋 < / div > < div className = "dash-card-value" > { systemInfo . task _count } < / div > < div className = "dash-card-label" > Agent Tasks < / div > < / div >
< div className = "dash-card" > < div className = "dash-card-icon" > ✅ < / div > < div className = "dash-card-value" > { systemInfo . completed _runs || 0 } < / div > < div className = "dash-card-label" > Completed < / div > < / div >
< div className = "dash-card" > < div className = "dash-card-icon" > ❌ < / div > < div className = "dash-card-value" > { systemInfo . failed _runs || 0 } < / div > < div className = "dash-card-label" > Failed < / div > < / div >
< div className = "dash-card" > < div className = "dash-card-icon" > ⚡ < / div > < div className = "dash-card-value" > { systemInfo . running _runs || 0 } < / div > < div className = "dash-card-label" > Running < / div > < / div >
< div className = "dash-card" > < div className = "dash-card-icon" > 💬 < / div > < div className = "dash-card-value" > { systemInfo . active _chat _sessions || 0 } < / div > < div className = "dash-card-label" > Active Chats < / div > < / div >
< div className = "dash-card" > < div className = "dash-card-icon" > { systemInfo . scheduler _running ? '🟢' : '🔴' } < / div > < div className = "dash-card-value" > { systemInfo . scheduler _running ? 'Active' : 'Stopped' } < / div > < div className = "dash-card-label" > Scheduler < / div > < / div >
< / > ) }
< / div >
{ /* Auth */ }
< div className = "dashboard-section" >
< h3 > Claude Authentication < / h3 >
< div className = "auth-panel" >
{ authStatus ? . status === 'logged_in' ? (
< div className = "auth-connected" >
< span className = "auth-icon" > ✅ < / span >
< div > < div className = "auth-title" > Connected to Claude < / div > < div className = "auth-sub" > { authStatus . account || 'Authenticated' } { authStatus . auth _method && ` ( ${ authStatus . auth _method } ) ` } < / div > < / div >
< button className = "btn btn-secondary btn-sm" onClick = { handleLogout } > Log Out < / button >
< / div >
) : (
< div className = "auth-token-panel" >
< div className = "auth-token-header" >
< span className = "auth-icon" > 🔐 < / span >
< div > < div className = "auth-title" > { authStatus ? . has _saved _token ? 'Update Token' : 'Authenticate' } < / div > < div className = "auth-sub" > Two options : < / div > < / div >
< / div >
< div className = "auth-methods" >
< div className = "auth-method" >
< div className = "auth-method-title" > Option 1 : Setup Token ( Claude Max / Pro ) < / div >
< div className = "auth-method-desc" > Run on TrueNAS shell : < / div >
< code className = "auth-cmd" > docker exec - it claude - persistent - agent claude setup - token < / code >
< div className = "auth-method-desc" > Paste the < code > sk - ant - oat01 - ... < / code > token below . < / div >
< / div >
< div className = "auth-method" >
< div className = "auth-method-title" > Option 2 : API Key < / div >
< div className = "auth-method-desc" > From < a href = "https://console.anthropic.com/settings/keys" target = "_blank" rel = "noreferrer" > console . anthropic . com < / a > < / div >
< / div >
< / div >
< form className = "auth-token-form" onSubmit = { handleSubmitToken } >
< div className = "auth-token-type-row" >
< label > < input type = "radio" name = "tt" value = "oauth_token" checked = { tokenType === 'oauth_token' } onChange = { ( ) => setTokenType ( 'oauth_token' ) } / > Setup Token < / label >
< label > < input type = "radio" name = "tt" value = "api_key" checked = { tokenType === 'api_key' } onChange = { ( ) => setTokenType ( 'api_key' ) } / > API Key < / label >
< / div >
< div className = "auth-token-input-row" >
< input type = "password" placeholder = { tokenType === 'oauth_token' ? 'sk-ant-oat01-...' : 'sk-ant-api03-...' } value = { authToken } onChange = { e => setAuthToken ( e . target . value ) } className = "auth-token-input" autoComplete = "off" / >
< button type = "submit" className = "btn btn-primary btn-sm" disabled = { tokenSubmitting || ! authToken . trim ( ) } > { tokenSubmitting ? 'Saving...' : 'Save' } < / button >
< / div >
< / form >
{ authStatus ? . has _saved _token && (
< div className = "auth-saved-info" > Saved : { authStatus . token _type === 'api_key' ? 'API Key' : 'OAuth Token' } < button className = "btn btn-secondary btn-xs" onClick = { handleLogout } > Clear < / button > < / div >
) }
< / div >
) }
< / div >
< / div >
{ /* MCP */ }
< div className = "dashboard-section" >
< h3 > MCP Servers < / h3 >
< div className = "mcp-panel" >
{ mcpServers ? . servers ? . length > 0 ? (
< div className = "mcp-list" >
{ mcpServers . servers . map ( ( srv , i ) => (
< div key = { i } className = "mcp-row" >
< span className = "mcp-status-dot connected" > < / span >
< span className = "mcp-name" > { srv . name || srv } < / span >
< span className = "mcp-details" > { srv . details || srv . url || '' } < / span >
< button className = "btn-icon" onClick = { ( ) => handleRemoveMcp ( srv . name || srv ) } title = "Remove" > ✕ < / button >
< / div >
) ) }
< / div >
) : < div className = "mcp-empty" > < p > No MCP servers configured . < / p > < / div > }
{ showAddMcp ? (
< form className = "mcp-add-form" onSubmit = { handleAddMcp } >
< div className = "mcp-form-row" >
< input type = "text" placeholder = "Server name" value = { mcpForm . name } onChange = { e => setMcpForm ( { ... mcpForm , name : e . target . value } ) } required / >
< select value = { mcpForm . server _type } onChange = { e => setMcpForm ( { ... mcpForm , server _type : e . target . value } ) } > < option value = "sse" > SSE < / option > < option value = "stdio" > Stdio < / option > < / select >
< / div >
{ mcpForm . server _type === 'sse' ? < input type = "url" placeholder = "https://..." value = { mcpForm . url } onChange = { e => setMcpForm ( { ... mcpForm , url : e . target . value } ) } required / > :
< input type = "text" placeholder = "Command" value = { mcpForm . command } onChange = { e => setMcpForm ( { ... mcpForm , command : e . target . value } ) } required / > }
< div className = "mcp-form-actions" > < button type = "submit" className = "btn btn-primary btn-sm" > Add < / button > < button type = "button" className = "btn btn-secondary btn-sm" onClick = { ( ) => setShowAddMcp ( false ) } > Cancel < / button > < / div >
< / form >
) : < button className = "btn btn-secondary btn-sm mcp-add-btn" onClick = { ( ) => setShowAddMcp ( true ) } > + Add MCP Server < / button > }
< / div >
< / div >
{ /* Usage */ }
< div className = "dashboard-section" >
< h3 > Usage < / h3 >
< div className = "usage-grid" >
{ usageStats ? ( < >
< div className = "usage-row" > < span className = "usage-label" > Total Runs < / span > < span className = "usage-value" > { usageStats . claude _runs _total ? ? '—' } < / span > < / div >
< div className = "usage-row" > < span className = "usage-label" > Sessions < / span > < span className = "usage-value" > { usageStats . session _count ? ? '—' } < / span > < / div >
< div className = "usage-row" > < span className = "usage-label" > Next Reset < / span > < span className = "usage-value" > { usageStats . next _reset ? new Date ( usageStats . next _reset ) . toLocaleDateString ( ) : '—' } < / span > < / div >
< div className = "usage-row" > < span className = "usage-label" > Days Until Reset < / span > < span className = "usage-value" > { usageStats . days _until _reset ? ? '—' } < / span > < / div >
< / > ) : < div className = "usage-loading" > Loading ... < / div > }
< / div >
< / div >
< / div >
) }
2026-04-04 22:40:51 -04:00
< / main >
< / div >
) ;
} ;
const TaskRuns = ( { taskId } ) => {
const [ runs , setRuns ] = useState ( [ ] ) ;
useEffect ( ( ) => {
2026-04-05 00:26:21 -04:00
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 ) ;
2026-04-04 22:40:51 -04:00
} , [ taskId ] ) ;
return (
< div className = "task-section" >
< h3 > Run History < / h3 >
2026-04-05 00:26:21 -04:00
{ runs . length === 0 ? < p className = "empty" > No runs yet < / p > : (
2026-04-04 22:40:51 -04:00
< div className = "runs-list" >
2026-04-05 00:26:21 -04:00
{ runs . map ( run => (
2026-04-04 22:40:51 -04:00
< div key = { run . run _id } className = "run-item" >
< div className = "run-header" >
< span className = { ` run-status ${ run . status } ` } > { run . status } < / span >
2026-04-05 00:13:37 -04:00
< span className = "run-time" > { new Date ( run . started _at ) . toLocaleString ( ) } < / span >
2026-04-04 22:40:51 -04:00
< / div >
2026-04-05 00:13:37 -04:00
{ run . output && < details > < summary > Output < / summary > < pre > { run . output } < / pre > < / details > }
{ run . error && < details > < summary > Error < / summary > < pre className = "error" > { run . error } < / pre > < / details > }
2026-04-04 22:40:51 -04:00
< / div >
) ) }
< / div >
) }
< / div >
) ;
} ;
export default App ;