import { useEffect, useState, useCallback, useRef } from 'react'; import CodeMirror from '@uiw/react-codemirror'; import { json } from '@codemirror/lang-json'; import { markdown } from '@codemirror/lang-markdown'; import { yaml } from '@codemirror/lang-yaml'; import { oneDark } from '@codemirror/theme-one-dark'; import type { OpenClawStatus, OpenClawAgent, OpenClawSkill, OpenClawLogs, OpenClawModels, FileTreeNode, ReadFileResult, AgentMemoryResult } from '../api/client'; import { api } from '../api/client'; // ═════════════════════════════════════════════════════════════════════════════════ // Tab definitions // ═════════════════════════════════════════════════════════════════════════════════ const TABS = [ { id: 'status', label: 'Status', icon: '🔋' }, { id: 'filesystem', label: 'Arborescence', icon: '📂' }, { id: 'config', label: 'Configuration', icon: '⚙️' }, { id: 'agents', label: 'Agents', icon: '🤖' }, { id: 'skills', label: 'Skills', icon: '🧩' }, { id: 'logs', label: 'Logs', icon: '📟' }, { id: 'models', label: 'Modèles', icon: '🧠' }, ] as const; type TabId = (typeof TABS)[number]['id']; // ═════════════════════════════════════════════════════════════════════════════════ // Main Page // ═════════════════════════════════════════════════════════════════════════════════ export default function OpenClawPage() { const [activeTab, setActiveTab] = useState('status'); const [status, setStatus] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { api.openclawStatus() .then(s => { setStatus(s); setLoading(false); }) .catch(() => setLoading(false)); }, []); if (loading) { return (
🐙
Connexion à OpenClaw…
); } return (
{/* Header */}
🐙

OpenClaw

{status?.gateway_online ? 'Gateway opérationnel' : 'Gateway hors ligne'} {status?.openclaw_type}
{/* Tab Bar */}
{TABS.map(tab => ( ))}
{/* Tab Content */}
{activeTab === 'status' && } {activeTab === 'filesystem' && } {activeTab === 'config' && } {activeTab === 'agents' && } {activeTab === 'skills' && } {activeTab === 'logs' && } {activeTab === 'models' && }
); } // ═════════════════════════════════════════════════════════════════════════════════ // Tab 1 — Status // ═════════════════════════════════════════════════════════════════════════════════ function StatusTab({ status }: { status: OpenClawStatus | null }) { if (!status) return ; const metrics = [ { label: 'Gateway', value: status.gateway_online ? 'En ligne' : 'Hors ligne', accent: status.gateway_online ? 'green' : 'red', icon: '🔌' }, { label: 'Mode', value: status.openclaw_type, accent: 'purple', icon: '⚡' }, { label: 'Bind', value: status.gateway_bind, accent: 'blue', icon: '🌐' }, { label: 'Port', value: String(status.gateway_port), accent: 'indigo', icon: '🔗' }, { label: 'Agents', value: String(status.agent_count), accent: 'amber', icon: '🤖' }, { label: 'Skills', value: String(status.skill_count), accent: 'teal', icon: '🧩' }, ]; return (
{/* Hero health card */}
{status.gateway_online ? '✅' : '❌'}

{status.gateway_online ? 'Gateway Opérationnel' : 'Gateway Hors Ligne'}

{status.foxy_home} — Port {status.gateway_port}

{status.gateway_mode} {status.openclaw_type} {status.config_exists && ( config ✓ )}
{/* Metrics grid */}
{metrics.map(m => ( ))}
); } function MetricCard({ label, value, accent, icon }: { label: string; value: string; accent: string; icon: string }) { const colorMap: Record = { green: { bg: 'from-green-500/10 to-green-600/5', text: 'text-green-400' }, red: { bg: 'from-red-500/10 to-red-600/5', text: 'text-red-400' }, purple: { bg: 'from-purple-500/10 to-purple-600/5', text: 'text-purple-400' }, blue: { bg: 'from-blue-500/10 to-blue-600/5', text: 'text-blue-400' }, indigo: { bg: 'from-indigo-500/10 to-indigo-600/5', text: 'text-indigo-400' }, amber: { bg: 'from-amber-500/10 to-amber-600/5', text: 'text-amber-400' }, teal: { bg: 'from-teal-500/10 to-teal-600/5', text: 'text-teal-400' }, }; const c = colorMap[accent] || colorMap.purple; return (
{icon}
{label}
{value}
); } function TreeNode({ node, depth, onSelect }: { node: FileTreeNode; depth: number, onSelect?: (n: FileTreeNode) => void }) { const [open, setOpen] = useState(depth < 1); const isDir = node.type === 'directory'; const indent = depth * 20; return (
{ if (isDir) setOpen(!open); else onSelect?.(node); }} > {isDir ? ( {open ? '📂' : '📁'} ) : ( 📄 )} {node.name} {node.size != null && !isDir && ( {formatSize(node.size)} )}
{isDir && open && node.children?.map(child => ( ))}
); } function formatSize(bytes: number): string { if (bytes < 1024) return `${bytes} B`; if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`; return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; } // ═════════════════════════════════════════════════════════════════════════════════ // Tab 2 — Configuration // ═════════════════════════════════════════════════════════════════════════════════ function ConfigTab() { const [config, setConfig] = useState | null>(null); const [loading, setLoading] = useState(true); const [editing, setEditing] = useState(false); const fetchConfig = () => { setLoading(true); api.openclawConfig() .then(d => { setConfig(d.config); setLoading(false); }) .catch(() => setLoading(false)); }; useEffect(() => { fetchConfig(); }, []); if (loading) return ; if (!config) return ; const sections = Object.entries(config); return (
{editing && setEditing(false)} onSave={() => { setEditing(false); fetchConfig(); }} />} {/* Premium Header */}

⚙️ Configuration Globale

Paramètres système chargés depuis openclaw.json

{/* Grid of config sections */}
{sections.map(([key, value]) => { const isObject = typeof value === 'object' && value !== null; return (

{key.toUpperCase()}

{isObject && ( {Array.isArray(value) ? `${(value as unknown[]).length} items` : `${Object.keys(value as object).length} clés`} )}
{isObject ? (
                    {JSON.stringify(value, null, 2)}
                  
) : (
{renderValue(value)}
)}
); })}
); } function renderValue(v: unknown): string { if (v === null) return 'null'; if (typeof v === 'boolean') return v ? 'true' : 'false'; if (typeof v === 'number') return String(v); if (typeof v === 'string') return v.length > 80 ? v.slice(0, 77) + '…' : v; return String(v); } // ═════════════════════════════════════════════════════════════════════════════════ // Tab 3 — Agents (with Memory Viewer) // ═════════════════════════════════════════════════════════════════════════════════ function AgentMemoryViewer({ agentName }: { agentName: string }) { const [memory, setMemory] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { let active = true; api.openclawAgentMemory(agentName) .then(res => { if (active) { setMemory(res); setLoading(false); } }) .catch(err => { console.error(err); if (active) setLoading(false); }); return () => { active = false; }; }, [agentName]); if (loading) { return (

Connexion à la mémoire QMD de l'agent...

); } if (!memory || !memory.has_db) { return (
🗄️

Mémoire Introuvable

Aucune base de données SQLite QMD n'a été trouvée pour cet agent dans le workspace. Lumina ne dispose pas (encore) d'une trame chronologique ici.

Astuce : Vérifiez la configuration du dossier workspace sous FOXY_HOME.

); } return (

🧠 Labyrinth Memoire QMD

{memory.db_path} ({formatSize(memory.size || 0)})

{memory.stats.total_thoughts}
Pensées / Actions
{memory.timeline.length === 0 ? (
La mémoire existe mais elle est actuellement vide.
) : (
{memory.timeline.map((item: Record, idx: number) => (
{/* Node dot */}
{/* Content Card */}
{item.timestamp || item.created_at || 'Inconnu'} {(item.type || item.event_type || item.role) && ( {item.type || item.event_type || item.role} )}
                    {item.content || item.message || JSON.stringify(item, null, 2)}
                  
))}
)}
); } function AgentsTab() { const [agents, setAgents] = useState([]); const [loading, setLoading] = useState(true); const [expandedAgent, setExpandedAgent] = useState(null); const [activeView, setActiveView] = useState<'identity' | 'memory'>('identity'); const [deleting, setDeleting] = useState(null); const [activeFile, setActiveFile] = useState<{agent: string, file: string} | null>(null); const [fileContent, setFileContent] = useState(''); const [fileLoading, setFileLoading] = useState(false); const [saving, setSaving] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); const loadAgents = () => { setLoading(true); api.openclawAgents() .then(d => { setAgents(d.agents); setLoading(false); }) .catch(() => setLoading(false)); }; useEffect(() => { loadAgents(); }, []); const handleDelete = async (e: React.MouseEvent, name: string) => { e.stopPropagation(); if (!confirm(`Voulez-vous vraiment supprimer l'agent "${name}" ?\n\n⚠️ Cette action supprimera définitivement son dossier de configuration.`)) return; setDeleting(name); try { await api.openclawDeleteAgent(name); if (expandedAgent === name) setExpandedAgent(null); loadAgents(); } catch(err) { console.error(err); alert('Erreur lors de la suppression: ' + String(err)); } finally { setDeleting(null); } }; const openFile = async (agentName: string, filename: string) => { if (activeFile?.agent === agentName && activeFile?.file === filename) { setActiveFile(null); // toggle close setIsFullscreen(false); return; } setActiveFile({ agent: agentName, file: filename }); setIsFullscreen(false); setFileLoading(true); try { const data = await api.openclawReadAgentFile(agentName, filename); setFileContent(data.content || ''); } catch(err) { console.error(err); setFileContent('Erreur de chargement du fichier.'); } finally { setFileLoading(false); } }; const saveFile = async () => { if (!activeFile) return; setSaving(true); try { await api.openclawWriteAgentFile(activeFile.agent, activeFile.file, fileContent); loadAgents(); // refresh file sizes setActiveFile(null); // close the editor after saving } catch(err) { console.error(err); alert('Erreur lors de la sauvegarde: ' + String(err)); } finally { setSaving(false); } }; if (loading && agents.length === 0) return ; if (agents.length === 0) return ; return (
{/* Agent count header */}
🤖 {agents.length} agents détectés dans le répertoire OpenClaw
{/* Agent grid */}
{agents.map(agent => (
{ setExpandedAgent(expandedAgent === agent.name ? null : agent.name); setActiveFile(null); setIsFullscreen(false); setActiveView('identity'); }} >
🤖

{agent.name}

{agent.type} {agent.model && ( {agent.model} )} {agent.has_workspace && ( ws ✓ )}
{expandedAgent === agent.name ? '▲' : '▼'}
{/* Expanded details */} {expandedAgent === agent.name && (
{/* View Tabs */}
{activeView === 'memory' ? ( ) : ( <> {/* Agent Identity Files Manager */} {agent.has_workspace && agent.identity_files && agent.identity_files.length > 0 && (

⚙️ Fichiers d'identité

{agent.identity_files.map(file => { const isActive = activeFile?.agent === agent.name && activeFile?.file === file.name; return ( ); })}
{/* Identity File Editor */} {activeFile?.agent === agent.name && (
workspace/{agent.name}/{activeFile.file}
{fileLoading ? (
Chargement…
) : (
setFileContent(val)} basicSetup={{ lineNumbers: true, foldGutter: false, highlightActiveLine: true, tabSize: 2 }} />
)}
)}
)} {/* System Prompt (Read-only overlay for old config style) */} {agent.system_prompt && !activeFile && (

Instructions principales

{agent.system_prompt}
)} {/* Configuration (Read-only) */} {agent.config && !activeFile && activeView === 'identity' && (

Configuration JSON/YAML {agent.config_files?.join(', ')}

                      {JSON.stringify(agent.config, null, 2)}
                    
)} )}
)}
))}
); } // ═════════════════════════════════════════════════════════════════════════════════ // Tab 4 — Skills // ═════════════════════════════════════════════════════════════════════════════════ function SkillsTab() { const [skills, setSkills] = useState([]); const [loading, setLoading] = useState(true); const [expandedSkill, setExpandedSkill] = useState(null); useEffect(() => { api.openclawSkills() .then(d => { setSkills(d.skills); setLoading(false); }) .catch(() => setLoading(false)); }, []); if (loading) return ; if (skills.length === 0) return ; return (
🧩 {skills.length} skills disponibles
{skills.map(skill => (
setExpandedSkill(expandedSkill === skill.name ? null : skill.name)} >
🧩

{skill.name}

{skill.type} {skill.source}
{skill.description_short && (

{skill.description_short}

)} {skill.subdirs && skill.subdirs.length > 0 && (
{skill.subdirs.map(d => ( 📁 {d} ))}
)}
{skill.file_count != null && {skill.file_count} fichiers} {skill.size != null && {formatSize(skill.size)}}
{expandedSkill === skill.name && skill.description && (

Description

                  {skill.description}
                
)}
))}
); } // ═════════════════════════════════════════════════════════════════════════════════ // Tab 5 — Logs (Terminal style) // ═════════════════════════════════════════════════════════════════════════════════ function LogsTab() { const [logs, setLogs] = useState(null); const [loading, setLoading] = useState(true); const [levelFilter, setLevelFilter] = useState(''); const [lineCount, setLineCount] = useState(200); const [autoRefresh, setAutoRefresh] = useState(false); const logEndRef = useRef(null); const fetchLogs = useCallback(async () => { try { const params: { lines: number; level?: string } = { lines: lineCount }; if (levelFilter) params.level = levelFilter; const data = await api.openclawLogs(params); setLogs(data); } catch { /* ignore */ } setLoading(false); }, [lineCount, levelFilter]); useEffect(() => { fetchLogs(); }, [fetchLogs]); useEffect(() => { if (!autoRefresh) return; const interval = setInterval(fetchLogs, 5000); return () => clearInterval(interval); }, [autoRefresh, fetchLogs]); useEffect(() => { if (logEndRef.current) { logEndRef.current.scrollIntoView({ behavior: 'smooth' }); } }, [logs]); function getLineStyle(line: string): string { const upper = line.toUpperCase(); if (upper.includes('ERROR') || upper.includes('FATAL') || upper.includes('PANIC')) return 'text-red-400'; if (upper.includes('WARN')) return 'text-yellow-400'; if (upper.includes('DEBUG') || upper.includes('TRACE')) return 'text-gray-600'; if (upper.includes('INFO')) return 'text-green-300'; return 'text-gray-400'; } return (
{/* Toolbar */}
📟 Gateway Logs
{/* Level filter */}
{['', 'INFO', 'WARNING', 'ERROR'].map(lvl => ( ))}
{/* Line count */} {/* Auto-refresh toggle */} {/* Refresh button */}
{/* Terminal */} {loading ? : (
{/* Terminal header */}
{logs?.log_path || 'gateway.log'} {logs && ( {logs.showing} / {logs.total_lines} lignes )}
{/* Terminal content */}
{logs?.error && (
{logs.error}
)} {(!logs?.lines || logs.lines.length === 0) ? (
Aucune ligne de log
) : ( logs.lines.map((line, i) => (
{i + 1} {line}
)) )}
)}
); } // ═════════════════════════════════════════════════════════════════════════════════ // Tab 6 — Models & Providers // ═════════════════════════════════════════════════════════════════════════════════ function ModelsTab() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [editing, setEditing] = useState(false); const fetchModels = () => { setLoading(true); api.openclawModels() .then(d => { setData(d); setLoading(false); }) .catch(() => setLoading(false)); }; useEffect(() => { fetchModels(); }, []); if (loading) return ; if (!data || data.error) return ; return (
{editing && setEditing(false)} onSave={() => { setEditing(false); fetchModels(); }} />} {/* Premium Header */}

🧠 Intelligence Artificielle (LLMs & MCP)

Fournisseurs, serveurs MCP et modèles configurés

{/* Providers Grid */} {data.providers.length > 0 && (

🔌 Providers / MCP Servers

{data.providers.map((p, i) => (
{p.type === 'mcp' ? '🧱' : '💬'}

{String(p.name || `Provider ${i + 1}`)}

{Boolean(p.type) && ( {String(p.type)} )} {p.enabled !== undefined && ( {p.enabled ? 'actif' : 'inactif'} )}
{Boolean(p.model) && (
modèle: {String(p.model)}
)} {Boolean(p.endpoint) && (
{String(p.endpoint)}
)}
))}
)} {/* Models Table */} {data.models.length > 0 && (

🤖 Modèles Configurés

{data.models.map((m, i) => ( ))}
Identifiant Modèle Détails configuration (JSON)
🏷️ {String(m.name || `Model ${i + 1}`)}
                        {JSON.stringify(m, null, 2)}
                      
)} {/* Raw config keys */} {data.raw_keys && data.raw_keys.length > 0 && (

🔑 Clés racines détectées :

{data.raw_keys.map(k => ( {k} ))}
)} {data.providers.length === 0 && data.models.length === 0 && ( )}
); } // ═════════════════════════════════════════════════════════════════════════════════ // Shared Components // ═════════════════════════════════════════════════════════════════════════════════ function LoadingSpinner() { return (
🐙
Chargement…
); } function EmptyState({ message }: { message: string }) { return (
🐙

{message}

); } // ═════════════════════════════════════════════════════════════════════════════════ // Shared Config Editor Overlay // ═════════════════════════════════════════════════════════════════════════════════ function ConfigFileEditor({ onClose, onSave }: { onClose: () => void, onSave: () => void }) { const [content, setContent] = useState(''); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); useEffect(() => { // Lock body scroll document.body.style.overflow = 'hidden'; api.openclawReadFile('openclaw.json') .then(res => { setContent(res.pretty || res.content || ''); setLoading(false); }) .catch(err => { console.error(err); setContent('Erreur de chargement du fichier openclaw.json'); setLoading(false); }); return () => { // Restore scroll when unmounting document.body.style.overflow = ''; }; }, []); const handleSave = async () => { setSaving(true); try { await api.openclawWriteFile('openclaw.json', content); onSave(); } catch (err) { console.error(err); alert('Erreur lors de la sauvegarde : ' + String(err)); } finally { setSaving(false); } }; return (
✏️

Édition directe

openclaw.json
{loading ? (
⚙️ Lecture de la configuration brute...
) : (
setContent(val)} basicSetup={{ lineNumbers: true, foldGutter: true, highlightActiveLine: true, tabSize: 2, }} />
)}
); } // ═════════════════════════════════════════════════════════════════════════════════ // Tab 7 — Filesystem (Arborescence) // ═════════════════════════════════════════════════════════════════════════════════ function FilesystemTab() { const [tree, setTree] = useState([]); const [treeLoading, setTreeLoading] = useState(true); const [selectedFile, setSelectedFile] = useState(null); const [fileLoading, setFileLoading] = useState(false); const [editMode, setEditMode] = useState(false); const [editContent, setEditContent] = useState(''); const [saving, setSaving] = useState(false); const [isFullscreen, setIsFullscreen] = useState(false); useEffect(() => { loadTree(); }, []); const loadTree = () => { setTreeLoading(true); api.openclawFilesystem() .then(d => { setTree(d.tree); setTreeLoading(false); }) .catch(() => setTreeLoading(false)); }; const handleSelectFile = async (node: FileTreeNode) => { if (node.type !== 'file') return; setFileLoading(true); setEditMode(false); try { const data = await api.openclawReadFile(node.path); setSelectedFile(data); setEditContent(data.pretty || data.content || ''); } catch (err) { console.error(err); } finally { setFileLoading(false); } }; const handleSave = async () => { if (!selectedFile) return; setSaving(true); try { await api.openclawWriteFile(selectedFile.path, editContent); const data = await api.openclawReadFile(selectedFile.path); setSelectedFile(data); setEditMode(false); } catch (err) { console.error(err); } finally { setSaving(false); } }; const getExtensions = () => { if (!selectedFile) return []; const lang = selectedFile.language; if (lang === 'json') return [json()]; if (lang === 'markdown') return [markdown()]; if (lang === 'yaml') return [yaml()]; return []; }; return (
{/* Sidebar Tree */}

📂 Arborescence

{treeLoading ? (
Chargement de l'arborescence…
) : tree.length === 0 ? (

Aucun fichier trouvé

) : (
{tree.map(node => ( ))}
)}
{/* Editor Panel */}
{fileLoading ? (
Ouverture du fichier…
) : !selectedFile ? (
📄

Sélectionnez un fichier pour le visualiser

) : (
{/* Editor Header */}

📄 {selectedFile.path.split('/').pop()}

{selectedFile.path} {formatSize(selectedFile.size)} {selectedFile.language}
{!selectedFile.binary && ( <> {editMode ? ( <> ) : ( )} )}
{/* Editor Body */}
{selectedFile.binary ? (
📦

Fichier binaire. Aperçu non disponible.

) : editMode ? (
setEditContent(val)} basicSetup={{ lineNumbers: true, foldGutter: true, highlightActiveLine: true, tabSize: 2, }} />
) : (
                      {selectedFile.pretty || selectedFile.content}
                    
)}
)}
); }