import { Fragment, useEffect, useState } from 'react';
import { useShallow } from 'zustand/react/shallow';
import {
AlertTriangle,
Wrench, CalendarClock, Sparkles, Layers, History, Play, Pause, Trash2, Pencil, Plus, Save, X, RefreshCw,
CheckCircle2, XCircle, Loader2, Bot, Cpu, GitBranch, Webhook, FolderOpen
} from 'lucide-react';
import { formatDateTime, formatTime, previewText } from '../../lib/text';
import { bridge } from '../../lib/bridge';
import { jobSchedule, jobOutput } from '../../services/hermes/client';
import type { HermesJob, HermesJobDraft } from '../../services/hermes/types';
import { useChat } from '../../state/chat';
import { jobStatus, useHermes } from '../../state/hermes';
import { useSettings } from '../../state/settings';
type Tab = 'activity' | 'jobs' | 'skills' | 'sessions' | 'link';
const emptyDraft = (): HermesJobDraft => ({ name: '', schedule: 'every 1h', prompt: '', deliver: 'local' });
function ActivityTab() {
const activity = useChat((s) => s.activity);
const items = [...activity].reverse();
if (items.length === 0) return
Aucune activité outil pour l’instant.
;
return (
{items.map((a) => (
{a.status === 'running' ? : a.status === 'done' ? : }
{a.kind === 'subagent' ? <> > : null}{a.name}
{a.detail &&
{previewText(a.detail, 220)}
}
{a.output &&
{previewText(a.output, 220)}
}
{formatTime(a.startedAt)}{a.endedAt ? ` · ${((a.endedAt - a.startedAt) / 1000).toFixed(1)}s` : ''}
))}
);
}
function JobsTab() {
const jobs = useHermes((s) => s.jobs);
const jobsError = useHermes((s) => s.jobsError);
const busy = useHermes((s) => s.busy);
const lastSyncAt = useHermes((s) => s.lastSyncAt);
const refreshJobs = useHermes((s) => s.refreshJobs);
const createJob = useHermes((s) => s.createJob);
const updateJob = useHermes((s) => s.updateJob);
const jobAction = useHermes((s) => s.jobAction);
const setError = useChat((s) => s.setError);
const [draft, setDraft] = useState(emptyDraft);
const [editing, setEditing] = useState(null);
const [showEditor, setShowEditor] = useState(false);
const run = async (task: Promise) => {
try {
await task;
} catch (err) {
setError(`Cron Hermes : ${(err as Error).message}`);
}
};
const startEdit = (job: HermesJob) => {
setEditing(job.id);
setDraft({ name: job.name ?? job.id, schedule: jobSchedule(job), prompt: job.prompt ?? '', deliver: job.deliver ?? 'local' });
setShowEditor(true);
};
const save = async () => {
if (editing) await run(updateJob(editing, draft));
else await run(createJob(draft));
setEditing(null);
setDraft(emptyDraft());
setShowEditor(false);
};
const valid = draft.name.trim() && draft.schedule.trim() && draft.prompt.trim();
return (
{lastSyncAt ? `sync ${formatTime(lastSyncAt)}` : 'non synchronisé'}
void run(refreshJobs())}>
{ setEditing(null); setDraft(emptyDraft()); setShowEditor((v) => !v); }}> Cron
{showEditor && (
)}
{jobsError &&
Crons indisponibles : {jobsError}
}
{jobs.length === 0 ? (
{jobsError ? 'La liaison Hermes reste utilisable ; seule la planification est indisponible.' : 'Aucun cron Hermes. Créez une mission planifiée en langage naturel.'}
) : (
{jobs.map((job) => {
const status = jobStatus(job).toLowerCase();
const paused = status === 'paused' || job.enabled === false;
const output = jobOutput(job);
return (
{job.name || job.id}
{status}
{job.last_status && {String(job.last_status)} }
{jobSchedule(job)} · prochain {formatDateTime(job.next_run_at)}
{job.prompt &&
{previewText(job.prompt, 140)} }
{output &&
↳ {previewText(output, 120)} }
void run(jobAction(job.id, 'run'))}>
void run(jobAction(job.id, paused ? 'resume' : 'pause'))}>{paused ? : }
startEdit(job)}>
{ if (confirm(`Supprimer le cron « ${job.name || job.id} » ?`)) void run(jobAction(job.id, 'delete')); }}>
);
})}
)}
);
}
function HistoryList() {
const runs = useHermes((s) => s.jobRuns);
const markRead = useHermes((s) => s.markRunRead);
const addMessage = useChat((s) => s.addMessage);
if (runs.length === 0) return null;
return (
<>
Résultats récents
{runs.slice(0, 20).map((r) => (
{ if (e.key === 'Enter') (e.currentTarget as HTMLDivElement).click(); }}
onClick={() => {
markRead(r.id);
addMessage({ role: 'assistant', content: r.output, source: 'cron', jobName: r.jobName, status: 'done', timestamp: new Date(r.at).getTime() || Date.now() });
}}
>
{r.jobName}
{r.status}
{previewText(r.output, 160)}
{formatDateTime(r.at)}
))}
>
);
}
function SkillsTab() {
const skills = useHermes((s) => s.skills);
const toolsets = useHermes((s) => s.toolsets);
const refresh = useHermes((s) => s.refreshCatalog);
const [filter, setFilter] = useState('');
const q = filter.toLowerCase();
const visibleSkills = skills.filter((s) => !q || `${s.name} ${s.description ?? ''} ${s.category ?? ''}`.toLowerCase().includes(q));
const visibleToolsets = toolsets.filter((t) => !q || `${t.name} ${t.label ?? ''} ${t.description ?? ''} ${(t.tools ?? []).join(' ')}`.toLowerCase().includes(q));
return (
setFilter(e.target.value)} />
void refresh()}>
Skills ({skills.length})
{visibleSkills.length === 0 ?
Aucun skill exposé par ce serveur.
: (
{visibleSkills.map((s) => (
{s.name}
{s.category && {s.category} }
{s.description &&
{s.description} }
))}
)}
Toolsets ({toolsets.length})
{visibleToolsets.length === 0 ?
Aucun toolset listé.
: (
{visibleToolsets.map((t) => (
{t.label || t.name}
{t.enabled !== undefined && {t.enabled ? 'actif' : 'inactif'} }
{t.configured === false && non configuré }
{t.description &&
{t.description} }
{t.tools && t.tools.length > 0 &&
{t.tools.slice(0, 12).join(' · ')}{t.tools.length > 12 ? ` · +${t.tools.length - 12}` : ''} }
))}
)}
);
}
function SessionsTab() {
const sessions = useHermes((s) => s.sessions);
const refresh = useHermes((s) => s.refreshSessions);
const client = useHermes((s) => s.client);
const transport = useHermes((s) => s.transport);
const sessionId = useSettings((s) => s.settings.hermesSessionId);
const setSessionId = useSettings((s) => s.setHermesSessionId);
const chat = useChat.getState();
const activeId = sessionId.replace(/^hs:/, '');
const load = async (id: string) => {
try {
const messages = await client().sessionMessages(id);
chat.clear();
for (const m of messages.slice(-60)) {
if (!m.content) continue;
chat.addMessage({ role: m.role === 'user' ? 'user' : 'assistant', content: String(m.content), status: 'done', source: 'hermes', timestamp: m.timestamp ? new Date(m.timestamp).getTime() : Date.now() });
}
setSessionId(transport === 'sessions' ? `hs:${id}` : id);
} catch (err) {
chat.setError(`Session : ${(err as Error).message}`);
}
};
const fresh = () => {
chat.clear();
setSessionId(`eveflow-${Date.now().toString(36)}`);
};
return (
session {previewText(activeId, 28)}
void refresh()}>
Nouvelle
{sessions.length === 0 ?
Aucune session listée par Hermes.
: (
{sessions.map((s) => (
void load(s.id)} onKeyDown={(e) => { if (e.key === 'Enter') void load(s.id); }}>
{s.title || s.id}
{[s.platform ?? s.source, s.message_count ? `${s.message_count} msg` : null, formatDateTime(s.updated_at ?? s.created_at)].filter(Boolean).join(' · ')}
{ e.stopPropagation(); client().forkSession(s.id).then(() => refresh()).catch((err: Error) => chat.setError(err.message)); }}>
{ e.stopPropagation(); if (confirm('Supprimer cette session Hermes ?')) client().deleteSession(s.id).then(() => refresh()).catch((err: Error) => chat.setError(err.message)); }}>
))}
)}
);
}
function LinkTab() {
const { link, linkDetail, health, capabilities, transport, models, webhook } = useHermes(
useShallow((s) => ({ link: s.link, linkDetail: s.linkDetail, health: s.health, capabilities: s.capabilities, transport: s.transport, models: s.models, webhook: s.webhook }))
);
const connect = useHermes((s) => s.connect);
const hermes = useSettings((s) => s.settings.hermes);
const [info, setInfo] = useState<{ sharedDir: string; logPath: string; version: string } | null>(null);
useEffect(() => {
bridge()?.system.appInfo().then(setInfo).catch(() => undefined);
}, []);
const checks = (health?.readiness as { checks?: Record } | undefined)?.checks ?? {};
const features = capabilities?.features ?? {};
return (
Liaison Hermes
État {link} {linkDetail}
URL {hermes.url || '—'}
Transport {transport}{hermes.transport === 'auto' ? ' (auto)' : ''}
Modèle {hermes.model || String(capabilities?.model ?? '—')}
{capabilities?.version ? <>Version {String(capabilities.version)} > : null}
Modèles {models.length ? models.map((m) => m.id).slice(0, 6).join(', ') + (models.length > 6 ? '…' : '') : '—'}
void connect()}> Reconnecter
{Object.keys(features).length > 0 && (
<>
Capacités
{Object.entries(features).map(([k, v]) => (
{k}{typeof v === 'string' ? `: ${v}` : ''}
))}
>
)}
{Object.keys(checks).length > 0 && (
<>
Santé
{Object.entries(checks).map(([k, v]) => (
{k} {typeof v === 'object' ? JSON.stringify(v) : String(v)}
))}
>
)}
Webhook entrant
État {webhook?.listening ? 'à l’écoute' : 'inactif'} {webhook?.error}
Endpoint POST http://<cette-machine>:{webhook?.port ?? 7842}{webhook?.path ?? '/eveflow/hook'}
Secret {webhook?.secretConfigured ? 'requis (X-EveFlow-Secret)' : 'aucun'}
{info && (
<>
Application
Version {info.version}
Partage { e.preventDefault(); bridge()?.files.openPath(info.sharedDir); }}>{info.sharedDir}
Journal { e.preventDefault(); bridge()?.files.showInFolder(info.logPath); }}>{info.logPath}
>
)}
);
}
export function OpsPanel() {
const [tab, setTab] = useState('activity');
const running = useChat((s) => s.activity.filter((a) => a.status === 'running').length);
const unread = useHermes((s) => s.jobRuns.filter((r) => !r.read).length);
const link = useHermes((s) => s.link);
const tabs: Array<{ id: Tab; label: string; icon: React.ReactNode; badge?: number }> = [
{ id: 'activity', label: 'Outils', icon: , badge: running },
{ id: 'jobs', label: 'Crons', icon: , badge: unread },
{ id: 'skills', label: 'Skills', icon: },
{ id: 'sessions', label: 'Sessions', icon: },
{ id: 'link', label: 'Liaison', icon: }
];
return (
{tabs.map((t) => (
setTab(t.id)}>
{t.icon} {t.label}
{t.badge ? {t.badge} : null}
))}
{tab === 'activity' &&
}
{tab === 'jobs' && (<>
>)}
{tab === 'skills' &&
}
{tab === 'sessions' &&
}
{tab === 'link' &&
}
);
}