agent-dashboard es un panel local para visualizar actividad de agentes de IA en tiempo real. Levanta un servidor FastAPI, observa logs y procesos locales de distintos agentes, y renderiza una sala de control animada en HTML Canvas donde cada agente aparece como un robot con estado, color, movimiento, particulas y burbujas de actividad.
El proyecto esta pensado para correr en local, en la maquina del usuario, sin base de datos y sin servicios externos. Su objetivo es ofrecer una vista rapida y visual de que agentes estan activos, cuantos procesos hay por agente y cual fue la ultima linea relevante detectada en sus logs.
- Caracteristicas principales
- Vista general
- Estructura del proyecto
- Requisitos
- Instalacion
- Uso rapido
- Como funciona
- Agentes soportados
- Estados del agente
- Servidor backend
- Frontend Canvas
- WebSocket
- Script de ventana flotante
- Configuracion
- Personalizacion
- Troubleshooting
- Desarrollo
- Seguridad y privacidad
- Limitaciones conocidas
- Roadmap sugerido
- Dashboard visual en tiempo real.
- Backend local con FastAPI.
- Canal WebSocket para enviar cambios de estado al navegador.
- Renderizado completo con HTML Canvas, sin framework frontend.
- Robots animados para cada agente.
- Soporte inicial para Hermes, Claude Code y Codex.
- Deteccion de multiples instancias por agente inspeccionando procesos locales con
ps aux. - Lectura de logs locales para inferir actividad.
- Transiciones automaticas entre estados
working,thinkingeidle. - Scripts
widget.shywidget.ps1para abrir el dashboard como ventana tipo app/widget con Chrome, Chromium o Edge. - Sin base de datos, sin build step y sin dependencias frontend.
El dashboard esta formado por tres piezas:
server.py: servidor Python que expone la pagina web y un WebSocket.index.html: experiencia visual animada con Canvas y JavaScript puro.widget.sh: helper para macOS/Linux que arranca el servidor si hace falta y abre Chrome/Chromium/Edge en modo app.widget.ps1: helper para Windows que hace lo mismo con Chrome o Edge.
Flujo simplificado:
Logs y procesos locales
|
v
server.py
- lee logs
- cuenta procesos
- calcula estados
- emite JSON por WebSocket
|
v
index.html
- recibe estados
- sincroniza robots
- dibuja la sala animada
agent-dashboard/
├── index.html # Interfaz visual, animaciones y cliente WebSocket
├── server.py # Backend FastAPI y monitor de agentes
├── widget.sh # Lanzador macOS/Linux para abrir el dashboard como app
└── widget.ps1 # Lanzador Windows para abrir el dashboard como app
El repositorio no incluye por ahora:
requirements.txtpyproject.toml- tests automatizados
- configuracion externa
- assets estaticos adicionales
Todo el frontend vive dentro de index.html, y todo el backend vive dentro de server.py.
El servidor es multiplataforma. Los lanzadores incluidos cubren:
- macOS/Linux:
widget.sh - Windows:
widget.ps1
Para que la ventana se vea lo mas parecida posible en todos los sistemas, los lanzadores priorizan navegadores Chromium en modo app: Google Chrome, Chromium o Microsoft Edge. No se puede garantizar igualdad pixel-perfect entre macOS, Linux y Windows porque intervienen WebGL, drivers GPU, escalado de pantalla y renderizado de fuentes, pero el dashboard usa los mismos assets locales y la misma ventana app.
Requiere Python 3.10 o superior recomendado.
Dependencias Python:
fastapi
uvicornPara usar el modo widget necesitas Chrome, Chromium o Edge. Tambien puedes abrir manualmente el dashboard en cualquier navegador moderno entrando a:
http://127.0.0.1:7788
Desde la carpeta del proyecto:
cd /Users/david/agent-dashboardCrea un entorno virtual, opcional pero recomendado:
python3 -m venv .venv
source .venv/bin/activateInstala las dependencias:
pip install fastapi uvicornComo el proyecto no incluye un requirements.txt, esas son las dependencias directas que necesita server.py.
cd /Users/david/agent-dashboard
python3 server.pyLuego abre:
http://127.0.0.1:7788
El servidor escucha en:
127.0.0.1:7788
macOS/Linux:
cd /Users/david/agent-dashboard
./widget.shWindows PowerShell:
cd C:\ruta\a\agent-dashboard
.\widget.ps1Estos scripts:
- Comprueba si ya hay algo escuchando en el puerto
7788. - Si no hay servidor, ejecuta
python3 server.pyen segundo plano. - Espera brevemente.
- Abre Chrome, Chromium o Edge en modo app apuntando a
http://127.0.0.1:7788.
El resultado es una ventana sin barra de navegador, redimensionable y movible como si fuese una app nativa.
El backend mantiene un diccionario global llamado agent_states con el estado de cada agente:
agent_states = {
"hermes": {"status": "idle", "last_line": "", "activity": 0, "instances": 1},
"claude": {"status": "idle", "last_line": "", "activity": 0, "instances": 1},
"codex": {"status": "idle", "last_line": "", "activity": 0, "instances": 1},
}Cada entrada contiene:
status: estado visual del agente.last_line: ultima linea o mensaje relevante detectado.activity: timestamp de la ultima actividad.instances: cantidad de procesos detectados para ese agente.
El servidor ejecuta varias tareas asincronas:
tail_log(...): sigue logs de Hermes y Codex.poll_claude(): inspecciona archivos JSONL de Claude Code.poll_instances(): cuenta sesiones de agente vivas inspeccionando procesos locales.decay_status(): degrada estados con el paso del tiempo.broadcast(): envia el estado actual a todos los clientes WebSocket conectados.
El frontend recibe ese JSON, crea o elimina robots segun el numero de instancias, actualiza estados y muestra la ultima linea detectada como burbuja sobre el robot principal de cada agente.
Los agentes configurados actualmente son:
| Agente | ID interno | Color UI | Fuente de actividad | Patron de proceso |
|---|---|---|---|---|
| Hermes | hermes |
Verde neon | ~/.hermes/logs/agent.log |
CLI en terminal: venv/bin/hermes, hermes |
| Claude Code | claude |
Naranja | ~/.claude/projects/**/*.jsonl |
CLI en terminal: opt/homebrew/bin/claude, claude, claude.exe |
| Codex | codex |
Violeta | ~/.codex/log/codex-tui.log |
CLI en terminal: local/bin/codex, codex, codex.exe |
| Ollama | ollama |
Azul cian | ~/Library/Logs/Ollama/server.log, ~/.ollama/logs/server.log, %LOCALAPPDATA%/Ollama/server.log |
ollama, ollama.exe |
Estas fuentes se definen en server.py:
LOG_SOURCES = {
"hermes": Path.home() / ".hermes/logs/agent.log",
"claude": Path.home() / ".claude/projects",
"codex": Path.home() / ".codex/log/codex-tui.log",
"ollama": first_existing(...),
}Y los patrones de proceso se definen aqui:
PROCESS_PATTERNS = {
"hermes": {"posix": {"commands": [...], "contains": [...], "require_tty": True}, ...},
"claude": {"posix": {"commands": [...], "contains": [...], "require_tty": True}, ...},
"codex": {"posix": {"commands": [...], "contains": [...], "require_tty": True}, ...},
"ollama": {"posix": {"commands": [...], "contains": [...], "require_tty": False}, ...},
}El dashboard usa tres estados principales:
Estado de reposo. El robot se muestra mas tenue, se mueve despacio y no emite actividad destacada.
Estado activo. Se asigna cuando el backend detecta una linea nueva en un log o un mensaje nuevo en Claude. Visualmente:
- el robot se ilumina mas;
- aumenta su velocidad;
- emite particulas;
- puede mostrar una burbuja con texto;
- puede conectarse con otros robots activos mediante haces de luz.
Estado intermedio. Se asigna automaticamente cuando un agente estuvo working, pero no tuvo actividad nueva durante unos segundos.
La logica actual de degradacion es:
working -> thinking despues de 4 segundos sin actividad
thinking -> idle despues de 12 segundos sin actividad
Esta logica vive en decay_status() dentro de server.py.
El backend esta implementado con FastAPI.
Endpoints:
| Metodo | Ruta | Descripcion |
|---|---|---|
GET |
/ |
Devuelve index.html como HTML |
| WebSocket | /ws |
Envia el estado de agentes en tiempo real |
Arranque:
uvicorn.run(app, host="127.0.0.1", port=7788, log_level="warning")El servidor solo escucha en localhost. Esto evita exponer el dashboard directamente a la red local.
El ciclo de vida de FastAPI crea las tareas al arrancar:
@asynccontextmanager
async def lifespan(_app):
asyncio.create_task(tail_log(LOG_SOURCES["hermes"], "hermes"))
asyncio.create_task(tail_log(LOG_SOURCES["codex"], "codex"))
asyncio.create_task(poll_claude())
asyncio.create_task(poll_instances())
asyncio.create_task(decay_status())
yieldEstas tareas corren de forma continua mientras el servidor esta vivo.
Para Hermes y Codex se usa una funcion tipo tail -f:
async def tail_log(path: Path, name: str):
...La funcion:
- Comprueba si el archivo existe.
- Abre el archivo.
- Se posiciona al final.
- Lee nuevas lineas.
- Actualiza
last_line,statusyactivity. - Emite el nuevo estado por WebSocket.
Importante: al posicionarse al final del archivo, el dashboard solo reacciona a lineas nuevas desde que el servidor esta corriendo.
Claude se trata de forma diferente. En vez de hacer tail de un unico archivo, busca el archivo JSONL mas reciente bajo:
~/.claude/projects
Luego inspecciona mensajes del asistente y toma contenido textual. El texto se recorta a 120 caracteres para evitar burbujas demasiado largas.
La interfaz esta escrita en JavaScript puro dentro de index.html.
No usa React, Vue, Svelte, bundlers, CSS externo ni assets. Todo se dibuja con Canvas 2D.
Partes principales:
- setup del canvas;
- dibujo de la sala;
- ventanas del fondo con lineas tipo codigo;
- suelo con perspectiva;
- particulas;
- clase
Robot; - haces de conexion entre robots activos;
- pool de robots sincronizado con el WebSocket;
- bucle de render con
requestAnimationFrame.
La sala usa una perspectiva falsa basada en coordenadas de mundo:
function worldToScreen(wx, wz) {
// wx: -1..1 izquierda-derecha
// wz: 0..1 fondo-frente
}Cada robot tiene una posicion wx y wz. La funcion transforma esas coordenadas en posicion de pantalla y escala visual. Asi, los robots del fondo se ven mas pequenos y los del frente mas grandes.
Cada robot se representa con la clase:
class Robot {
constructor(uid, agentId, instanceIdx) { ... }
}La configuracion visual base esta en:
const CONFIGS = {
hermes: { name:'Hermes', color:'#00ff88', glow:'rgba(0,255,136,', shade:'#00cc70' },
claude: { name:'Claude Code', color:'#ff6b35', glow:'rgba(255,107,53,', shade:'#cc5528' },
codex: { name:'Codex', color:'#a78bfa', glow:'rgba(167,139,250,', shade:'#8b6fd4' },
};Cada robot:
- se mueve de forma autonoma;
- cambia velocidad segun estado;
- tiene ojos, cuerpo, antena y panel de pecho;
- muestra nombre y estado;
- emite particulas cuando trabaja;
- muestra burbujas cuando llega una nueva linea de actividad.
El backend envia instances para cada agente. El frontend crea tantos robots como instancias detectadas:
function syncRobots(agentId, state) {
const needed = state.instances || 1;
...
}Si hay mas de una instancia, el nombre visual se numera:
Codex
Codex #2
Codex #3
Solo la primera instancia recibe la burbuja con last_line; las demas reflejan el estado general.
El cliente conecta a:
const ws = new WebSocket('ws://127.0.0.1:7788/ws');Cuando recibe datos:
ws.onmessage = e => {
const data = JSON.parse(e.data);
for (const [id, state] of Object.entries(data)) {
syncRobots(id, state);
}
};Si la conexion se cierra, intenta reconectar cada 2 segundos:
ws.onclose = () => setTimeout(connect, 2000);El payload enviado por el servidor tiene esta forma:
{
"hermes": {
"status": "idle",
"last_line": "",
"activity": 0,
"instances": 1
},
"claude": {
"status": "working",
"last_line": "Texto reciente del asistente",
"activity": 1710000000.0,
"instances": 1
},
"codex": {
"status": "thinking",
"last_line": "Ultima linea detectada",
"activity": 1710000000.0,
"instances": 2
}
}widget.sh automatiza el uso en macOS y Linux:
./widget.shContenido funcional:
- usa
lsof -ti:$DASHBOARD_PORTpara saber si el puerto esta ocupado; - si el puerto esta libre, arranca
python3 server.py; - en macOS usa
open -nacon Chrome, Edge o Chromium; - en Linux usa
google-chrome,google-chrome-stable,chromium,chromium-browseromicrosoft-edge; - abre el dashboard con
--app=...para que parezca una ventana nativa sin barra de navegador; - define tamano inicial de ventana
1200x750; - define posicion inicial
80,80; - deshabilita extensiones para esa ventana;
- usa un perfil temporal dedicado para evitar diferencias por extensiones o configuraciones del navegador.
widget.ps1 hace lo equivalente en Windows, priorizando Chrome y luego Edge. Si no hay navegador Chromium disponible, abre la URL con el navegador predeterminado.
Actualmente la configuracion esta codificada en los archivos fuente.
Hay que actualizar dos sitios:
En server.py:
uvicorn.run(app, host="127.0.0.1", port=7788, log_level="warning")En index.html:
const ws = new WebSocket('ws://127.0.0.1:7788/ws');Y si usas widget.sh o widget.ps1, tambien:
lsof -ti:7788
--app=http://127.0.0.1:7788Edita LOG_SOURCES en server.py:
LOG_SOURCES = {
"hermes": Path.home() / ".hermes/logs/agent.log",
"claude": Path.home() / ".claude/projects",
"codex": Path.home() / ".codex/log/codex-tui.log",
}Edita PROCESS_PATTERNS en server.py:
PROCESS_PATTERNS = {
"hermes": {"posix": ["venv/bin/hermes", "hermes"], "windows": ["hermes.exe", "hermes"]},
"claude": {"posix": ["opt/homebrew/bin/claude", "claude"], "windows": ["claude.exe", "claude"]},
"codex": {"posix": ["local/bin/codex", "codex"], "windows": ["codex.exe", "codex"]},
"ollama": {"posix": ["ollama"], "windows": ["ollama.exe", "ollama"]},
}En macOS y Linux los patrones se buscan dentro de la salida de:
ps auxEn Windows se usa:
tasklist /FO CSV /NHPara Hermes, Claude Code y Codex en macOS/Linux se exige TTY de terminal, por lo que no se cuentan apps de escritorio ni helpers de fondo. Ollama permite procesos sin TTY porque normalmente corre como servicio local.
Para anadir un agente nuevo hay que tocar backend y frontend.
En server.py:
- Agrega una entrada en
LOG_SOURCES. - Agrega una entrada en
PROCESS_PATTERNS. - Agrega una entrada inicial en
agent_states. - Crea una tarea de lectura o polling en
lifespan().
Ejemplo:
LOG_SOURCES["nuevo"] = Path.home() / ".nuevo/logs/agent.log"
PROCESS_PATTERNS["nuevo"] = "nuevo-agent"
agent_states["nuevo"] = {"status": "idle", "last_line": "", "activity": 0, "instances": 1}En index.html, agrega una configuracion visual:
const CONFIGS = {
...
nuevo: { name:'Nuevo', color:'#00d5ff', glow:'rgba(0,213,255,', shade:'#0099bb' },
};El ID debe coincidir exactamente entre backend y frontend.
Los colores de cada agente estan en CONFIGS dentro de index.html.
Ejemplo:
codex: {
name:'Codex',
color:'#a78bfa',
glow:'rgba(167,139,250,',
shade:'#8b6fd4'
}Algunos puntos utiles:
- Velocidad por estado: getter
worldSpeed. - Particulas: metodo
emitParticles. - Brillo: bloque
glowdentro dedraw(). - Burbujas: metodo
drawBubble. - Haces entre agentes: funcion
drawBeams. - Fondo y sala:
drawRoom,drawWindows,drawFloor.
Edita decay_status() en server.py:
if s["status"] == "working" and now - s["activity"] > 4:
s["status"] = "thinking"
elif s["status"] == "thinking" and now - s["activity"] > 12:
s["status"] = "idle"Instala las dependencias:
pip install fastapi uvicornSi usas entorno virtual, asegurate de activarlo antes:
source .venv/bin/activateComprueba que proceso lo usa:
lsof -i :7788Puedes cerrar ese proceso o cambiar el puerto en server.py, index.html y widget.sh.
Comprueba:
- que el servidor sigue corriendo;
- que el navegador puede conectar a
ws://127.0.0.1:7788/ws; - que los logs existen;
- que hay nuevas lineas en los logs despues de arrancar el servidor.
tail_log() se posiciona al final del archivo. Si el log ya tenia contenido antes de arrancar el servidor, no lo mostrara como actividad nueva. Genera actividad nueva en el agente y observa si cambia el dashboard.
Tambien verifica que existan estas rutas:
~/.hermes/logs/agent.log
~/.codex/log/codex-tui.log
Claude se lee desde:
~/.claude/projects
El servidor busca archivos *.jsonl, toma el mas reciente y extrae mensajes con:
{
"message": {
"role": "assistant",
"content": "..."
}
}Si el formato cambia, puede que get_latest_claude_line() necesite actualizarse.
El contador usa:
ps auxDespues filtra las lineas que contienen el patron configurado en PROCESS_PATTERNS y solo cuenta procesos asociados a una TTY que empiece por s. Si el contador parece incorrecto, ajusta PROCESS_PATTERNS para que el patron coincida con el binario real que quieres contar.
En macOS, comprueba que Google Chrome, Microsoft Edge o Chromium existen como app instalada. Por ejemplo:
open -na "Google Chrome"En Linux, comprueba que existe alguno de estos comandos:
command -v google-chrome
command -v chromium
command -v microsoft-edgeEn Windows, widget.ps1 busca Chrome y Edge en las rutas habituales. Si usas otro navegador, ejecuta python3 server.py y abre manualmente:
http://127.0.0.1:7788
Hazlo ejecutable:
chmod +x widget.shpython3 server.pypython3 -m py_compile server.pyfind . -maxdepth 2 -type f- Modifica
server.pysi el cambio afecta datos, agentes, procesos o WebSocket. - Modifica
index.htmlsi el cambio afecta visualizacion, animacion o comportamiento del cliente. - Reinicia
server.py. - Recarga el navegador.
- Genera actividad real en alguno de los agentes.
- Verifica que el robot cambia de estado y vuelve a reposo con el tiempo.
El dashboard lee informacion local de logs de agentes. Eso puede incluir texto sensible generado por sesiones de trabajo.
Consideraciones:
- El servidor escucha solo en
127.0.0.1. - No envia datos a servicios externos.
- No guarda historico propio.
- Emite por WebSocket la ultima linea detectada para cada agente.
- Cualquier navegador local conectado al WebSocket puede ver esos mensajes.
No cambies el host a 0.0.0.0 salvo que entiendas el riesgo. Si se expone en red, otros dispositivos podrian acceder al dashboard y ver fragmentos de actividad.
- No hay archivo de dependencias versionado.
- No hay autenticacion.
- No hay tests automatizados.
- El puerto esta hardcodeado.
- El cliente WebSocket usa una URL fija.
- La configuracion de agentes esta repartida entre backend y frontend.
- Los logs de Hermes y Codex se leen solo si el archivo existe al arrancar la tarea.
- La deteccion de instancias mediante busqueda textual sobre
ps auxotasklistpuede producir falsos positivos si los patrones son demasiado amplios. - Claude depende del formato actual de los archivos JSONL en
~/.claude/projects. widget.shcubre macOS/Linux ywidget.ps1cubre Windows, pero la igualdad visual absoluta depende del motor Chromium, GPU, drivers y escalado del sistema.
Ideas razonables para evolucionar el proyecto:
- Crear
requirements.txtopyproject.toml. - Mover agentes, colores, rutas y patrones a un archivo de configuracion.
- Exponer el puerto mediante variable de entorno.
- Servir la URL WebSocket de forma relativa al host actual.
- Anadir endpoint
/health. - Mostrar estado de conexion en la UI.
- Anadir modo debug con JSON visible.
- Guardar un pequeno historial de ultimos mensajes.
- Permitir ocultar o mostrar agentes.
- Anadir tests unitarios para parsing de logs y estados.
- Anadir soporte para logs que aparecen despues de arrancar el servidor.
- Evitar falsos positivos en conteo de procesos usando una estrategia mas estricta.
agent-dashboard es una aplicacion local minimalista pero expresiva:
- Python se encarga de observar el sistema.
- FastAPI mantiene el canal HTTP/WebSocket.
- JavaScript dibuja una interfaz viva en Canvas.
- Los logs y procesos locales son la fuente de verdad.
- El navegador solo refleja el estado que el backend calcula.
Es un buen punto de partida para una sala de control personal de agentes, especialmente si se quiere visualizar actividad de varias herramientas de IA sin depender de servicios externos ni montar una arquitectura pesada.