Skip to Content
DocumentaçãoArquiteturaDashboard Realtime

🔴 AIOS Dashboard - Arquitetura de Observabilidade em Tempo Real

PT | EN | ES

Versão: 1.0.0 Data: 2026-01-29 Status: Proposta Autor: @architect (Aria) Relacionado: dashboard-architecture.md


📋 Índice

  1. Visão Geral
  2. Problema Atual
  3. Arquitetura Proposta
  4. Event Emitter (CLI)
  5. Events Schema
  6. Enhanced SSE Endpoint
  7. Events Store
  8. Novos Componentes UI
  9. Fluxo de Dados Completo
  10. Implementação Faseada

Visão Geral

Este documento descreve a arquitetura para observabilidade em tempo real do AIOS Dashboard, permitindo que usuários acompanhem comandos executados no CLI com máximo detalhe visual.

Caso de Uso Principal

Usuário executa comandos no CLI → Dashboard mostra TUDO em tempo real

Princípios

  1. Zero Configuration - Funciona automaticamente quando CLI e Dashboard estão ativos
  2. File-Based - Comunicação via filesystem (não requer servidor adicional)
  3. Append-Only Events - Log de eventos imutável para debugging
  4. Graceful Degradation - Dashboard funciona mesmo sem eventos (fallback para polling)

Problema Atual

O que o Dashboard MOSTRA hoje

Evento no CLIDashboard AtualNota
@agent ativa✅ StatusBar mostraFunciona
*exit agent✅ Agent vai para standbyFunciona
Story status muda⚠️ Kanban atualizaSem notificação

O que o Dashboard NÃO MOSTRA

Evento no CLIDashboard Atual
Comando *xxx executando❌ Nada
Claude “pensando”❌ Nada
Tool calls (Read/Write/Bash)❌ Nada
Progresso da tarefa❌ Nada
Output do Claude❌ Nada
git commit/push❌ Nada
Erros❌ Nada
Tarefa completa❌ Nada

Gap Visual

┌─────────────────────────────────────────────────────────────────────────┐ │ FLUXO: CLI → Dashboard Real-Time │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ VOCÊ NO CLI DASHBOARD │ │ ──────────── ───────── │ │ │ │ @architect ─────────────────────────────▶ ✅ Agent ativo aparece │ │ (ativa agente) (StatusBar + AgentMonitor) │ │ │ │ *create-architecture ───────────────────▶ ❌ NÃO MOSTRA comando │ │ (executa tarefa) executando │ │ │ │ [Claude pensando...] ───────────────────▶ ❌ NÃO MOSTRA progresso │ │ em tempo real │ │ │ │ [Criando arquivo X] ────────────────────▶ ❌ NÃO MOSTRA arquivos │ │ [Editando arquivo Y] sendo criados/editados │ │ │ │ [Story atualizada] ─────────────────────▶ ⚠️ PARCIAL - status muda │ │ mas sem detalhes │ │ │ │ [git commit] ───────────────────────────▶ ❌ NÃO MOSTRA commits │ │ em tempo real │ │ │ │ *exit ──────────────────────────────────▶ ✅ Agent vai para standby │ │ │ └─────────────────────────────────────────────────────────────────────────┘

Arquitetura Proposta

Diagrama Geral

┌─────────────────────────────────────────────────────────────────────────┐ │ CLI / AIOS AGENTS │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ Claude Code Session │ │ │ │ @architect → *create-architecture → [thinking...] → [file ops] │ │ │ └────────────────────────────┬────────────────────────────────────┘ │ │ │ │ │ │ EMIT EVENTS │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ .aios/dashboard/events.jsonl (append-only) │ │ │ │ {"type":"agent:activated","agent":"architect","ts":"..."} │ │ │ │ {"type":"command:start","cmd":"*create-architecture","ts":"..."}│ │ │ │ {"type":"llm:thinking","duration":0,"ts":"..."} │ │ │ │ {"type":"tool:call","tool":"Read","file":"src/index.ts","ts":""}│ │ │ │ {"type":"file:write","path":"docs/arch.md","lines":50,"ts":""} │ │ │ │ {"type":"command:complete","cmd":"*create","success":true,"ts":""}│ │ │ └────────────────────────────┬────────────────────────────────────┘ │ │ │ │ └───────────────────────────────────┼──────────────────────────────────────┘ │ SSE Stream ┌─────────────────────────────────────────────────────────────────────────┐ │ DASHBOARD │ │ │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ /api/events (enhanced SSE) │ │ │ │ - Watch events.jsonl for changes │ │ │ │ - Stream new events to connected clients │ │ │ │ - Maintain last N events in memory │ │ │ └────────────────────────────┬──────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌───────────────────────────────────────────────────────────────────┐ │ │ │ events-store (NEW) │ │ │ │ - currentCommand: { name, startedAt, status } │ │ │ │ - llmStatus: 'idle' | 'thinking' | 'responding' │ │ │ │ - recentFiles: { path, action, timestamp }[] │ │ │ │ - recentEvents: Event[] (circular buffer) │ │ │ │ - errors: Error[] │ │ │ └────────────────────────────┬──────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ UI COMPONENTS │ │ │ │ │ │ │ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │ │ │ │ CommandPanel │ │ ActivityFeed │ │ FileChangesPanel│ │ │ │ │ │ ───────────── │ │ ──────────── │ │ ─────────────── │ │ │ │ │ │ *create-arch │ │ 02:45 Thinking │ │ ✏️ docs/arch.md │ │ │ │ │ │ ████████░░ 80% │ │ 02:44 Read x.ts │ │ ✏️ src/index.ts │ │ │ │ │ │ 2m 34s elapsed │ │ 02:43 Agent on │ │ 📁 +3 files │ │ │ │ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │ │ │ │ │ │ │ ┌───────────────────────────────────────────────────────────┐ │ │ │ │ │ TerminalStream (enhanced) │ │ │ │ │ │ Real-time Claude output with ANSI colors │ │ │ │ │ │ [02:45:12] Analyzing project structure... │ │ │ │ │ │ [02:45:15] Creating architecture document... │ │ │ │ │ │ [02:45:20] ✓ docs/architecture/system-arch.md created │ │ │ │ │ └───────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────────────┘

Event Emitter (CLI)

Localização

.aios-core/core/events/dashboard-emitter.ts

Interface

// .aios-core/core/events/types.ts /** * Apenas eventos de alto nível (Decision #2) * Focado em monitoramento, não debugging */ export type DashboardEventType = // Ciclo de vida do agente | 'agent:activated' | 'agent:deactivated' // Execução de comando | 'command:start' | 'command:complete' | 'command:error' // Atualizações de story | 'story:status-change' // Sessão | 'session:start' | 'session:end'; export interface DashboardEvent { id: string; // UUID v4 type: DashboardEventType; timestamp: string; // ISO 8601 agentId?: string; // Agente ativo quando evento ocorreu sessionId?: string; // Identificador de sessão data: Record<string, unknown>; // Payload específico do evento }

Implementação

// .aios-core/core/events/dashboard-emitter.ts import { appendFileSync, existsSync, mkdirSync } from 'fs'; import { join } from 'path'; import { randomUUID } from 'crypto'; import type { DashboardEvent, DashboardEventType } from './types'; const EVENTS_DIR = '.aios/dashboard'; const EVENTS_FILE = 'events.jsonl'; const MAX_FILE_SIZE = 10 * 1024 * 1024; // rotação de 10MB class DashboardEmitter { private projectRoot: string; private sessionId: string; private activeAgentId: string | null = null; private enabled: boolean = true; constructor(projectRoot: string) { this.projectRoot = projectRoot; this.sessionId = randomUUID(); this.ensureDirectory(); } private ensureDirectory(): void { const dir = join(this.projectRoot, EVENTS_DIR); if (!existsSync(dir)) { mkdirSync(dir, { recursive: true }); } } private getEventsPath(): string { return join(this.projectRoot, EVENTS_DIR, EVENTS_FILE); } emit(type: DashboardEventType, data: Record<string, unknown> = {}): void { if (!this.enabled) return; const event: DashboardEvent = { id: randomUUID(), type, timestamp: new Date().toISOString(), agentId: this.activeAgentId ?? undefined, sessionId: this.sessionId, data, }; try { const line = JSON.stringify(event) + '\n'; appendFileSync(this.getEventsPath(), line, 'utf-8'); } catch (error) { // Falha silenciosa - dashboard é opcional console.debug('[DashboardEmitter] Failed to emit event:', error); } } // Métodos de conveniência de alto nível (Decision #2) agentActivated(agentId: string, agentName: string): void { this.activeAgentId = agentId; this.emit('agent:activated', { agentId, agentName }); } agentDeactivated(): void { const agentId = this.activeAgentId; this.activeAgentId = null; this.emit('agent:deactivated', { agentId }); } commandStart(command: string): void { this.emit('command:start', { command }); } commandComplete(command: string, success: boolean): void { this.emit('command:complete', { command, success }); } commandError(command: string, error: string): void { this.emit('command:error', { command, error }); } storyStatusChange(storyId: string, oldStatus: string, newStatus: string): void { this.emit('story:status-change', { storyId, oldStatus, newStatus }); } sessionStart(): void { this.emit('session:start', { sessionId: this.sessionId }); } sessionEnd(): void { this.emit('session:end', { sessionId: this.sessionId }); } // Métodos de controle disable(): void { this.enabled = false; } enable(): void { this.enabled = true; } } // Exportação Singleton let emitter: DashboardEmitter | null = null; export function getDashboardEmitter(projectRoot?: string): DashboardEmitter { if (!emitter && projectRoot) { emitter = new DashboardEmitter(projectRoot); } if (!emitter) { throw new Error('DashboardEmitter not initialized. Call with projectRoot first.'); } return emitter; } export function initDashboardEmitter(projectRoot: string): DashboardEmitter { emitter = new DashboardEmitter(projectRoot); return emitter; }

Integração com Claude Code Hooks

// .aios-core/integrations/claude-code/hooks.ts import { getDashboardEmitter } from '../core/events/dashboard-emitter'; /** * Apenas hooks de alto nível (Decision #2) * Eventos de ciclo de vida de agente e comando */ // Hook: Agente ativado (ex: @architect) export function onAgentActivated(agentId: string, agentName: string): void { const emitter = getDashboardEmitter(); emitter.agentActivated(agentId, agentName); } // Hook: Agente desativado (ex: *exit) export function onAgentDeactivated(): void { const emitter = getDashboardEmitter(); emitter.agentDeactivated(); } // Hook: Comando iniciado (ex: *create-architecture) export function onCommandStart(command: string): void { const emitter = getDashboardEmitter(); emitter.commandStart(command); } // Hook: Comando completo export function onCommandComplete(command: string, success: boolean): void { const emitter = getDashboardEmitter(); emitter.commandComplete(command, success); } // Hook: Erro no comando export function onCommandError(command: string, error: string): void { const emitter = getDashboardEmitter(); emitter.commandError(command, error); } // Hook: Story status muda export function onStoryStatusChange(storyId: string, oldStatus: string, newStatus: string): void { const emitter = getDashboardEmitter(); emitter.storyStatusChange(storyId, oldStatus, newStatus); }

Events Schema

Localização do Arquivo

.aios/dashboard/events.jsonl

Formato

JSON Lines (JSONL) - um objeto JSON por linha, apenas append.

Payloads de Eventos por Tipo (Apenas Alto Nível)

Eventos de Agente

{"id":"uuid","type":"agent:activated","timestamp":"2026-01-29T14:30:00.000Z","sessionId":"uuid","data":{"agentId":"architect","agentName":"Aria"}} {"id":"uuid","type":"agent:deactivated","timestamp":"2026-01-29T15:45:00.000Z","agentId":"architect","sessionId":"uuid","data":{"agentId":"architect"}}

Eventos de Comando

{"id":"uuid","type":"command:start","timestamp":"...","agentId":"architect","data":{"command":"*create-architecture"}} {"id":"uuid","type":"command:complete","timestamp":"...","agentId":"architect","data":{"command":"*create-architecture","success":true}} {"id":"uuid","type":"command:error","timestamp":"...","agentId":"architect","data":{"command":"*create-architecture","error":"Failed to read config file"}}

Eventos de Story

{ "id": "uuid", "type": "story:status-change", "timestamp": "...", "agentId": "architect", "data": { "storyId": "AIOS-123", "oldStatus": "in-progress", "newStatus": "review" } }

Eventos de Sessão

{"id":"uuid","type":"session:start","timestamp":"...","data":{"sessionId":"uuid"}} {"id":"uuid","type":"session:end","timestamp":"...","data":{"sessionId":"uuid"}}

Rotação de Arquivo

Quando events.jsonl excede 10MB:

  1. Renomear para events.[timestamp].jsonl
  2. Criar novo events.jsonl
  3. Manter últimos 5 arquivos rotacionados

Enhanced SSE Endpoint

Atual vs Aprimorado

AspectoCurrent /api/eventsAprimorado
Sourcestatus.json apenasstatus.json + events.jsonl
Trigger updatePolling intervalFile watch + polling
Event typesstatus:update apenasTodos os tipos de evento
HistoryNenhumÚltimos N eventos

Implementação

// apps/dashboard/src/app/api/events/route.ts (aprimorado) import { NextRequest } from 'next/server'; import { watch, existsSync, readFileSync, statSync } from 'fs'; import { join } from 'path'; import { Readable } from 'stream'; const AIOS_DIR = process.env.AIOS_PROJECT_ROOT || process.cwd(); const STATUS_FILE = join(AIOS_DIR, '.aios/dashboard/status.json'); const EVENTS_FILE = join(AIOS_DIR, '.aios/dashboard/events.jsonl'); interface SSEEvent { type: string; data: unknown; timestamp: string; } export async function GET(request: NextRequest): Promise<Response> { const encoder = new TextEncoder(); let lastEventPosition = 0; let isConnected = true; // Rastrear últimos tamanhos de arquivo para detecção de mudanças let lastStatusMtime = 0; let lastEventsSize = 0; const stream = new ReadableStream({ start(controller) { // Enviar evento inicial de conexão sendEvent(controller, { type: 'connection:status', data: { connected: true }, timestamp: new Date().toISOString(), }); // Enviar status atual sendCurrentStatus(controller); // Enviar eventos recentes (últimos 50) sendRecentEvents(controller, 50); // Setup de file watchers const watchers: ReturnType<typeof watch>[] = []; // Watch status.json if (existsSync(STATUS_FILE)) { const statusWatcher = watch(STATUS_FILE, (eventType) => { if (eventType === 'change' && isConnected) { const stat = statSync(STATUS_FILE); if (stat.mtimeMs > lastStatusMtime) { lastStatusMtime = stat.mtimeMs; sendCurrentStatus(controller); } } }); watchers.push(statusWatcher); } // Watch events.jsonl if (existsSync(EVENTS_FILE)) { const eventsWatcher = watch(EVENTS_FILE, (eventType) => { if (eventType === 'change' && isConnected) { const stat = statSync(EVENTS_FILE); if (stat.size > lastEventsSize) { sendNewEvents(controller, lastEventsSize); lastEventsSize = stat.size; } } }); watchers.push(eventsWatcher); lastEventsSize = statSync(EVENTS_FILE).size; } // Heartbeat a cada 30s const heartbeatInterval = setInterval(() => { if (isConnected) { sendEvent(controller, { type: 'heartbeat', data: { alive: true }, timestamp: new Date().toISOString(), }); } }, 30000); // Cleanup ao fechar request.signal.addEventListener('abort', () => { isConnected = false; clearInterval(heartbeatInterval); watchers.forEach((w) => w.close()); controller.close(); }); }, }); function sendEvent(controller: ReadableStreamDefaultController, event: SSEEvent): void { const data = `data: ${JSON.stringify(event)}\n\n`; controller.enqueue(encoder.encode(data)); } function sendCurrentStatus(controller: ReadableStreamDefaultController): void { try { if (existsSync(STATUS_FILE)) { const content = readFileSync(STATUS_FILE, 'utf-8'); const status = JSON.parse(content); sendEvent(controller, { type: 'status:update', data: status, timestamp: new Date().toISOString(), }); } } catch (error) { sendEvent(controller, { type: 'error', data: { message: 'Failed to read status' }, timestamp: new Date().toISOString(), }); } } function sendRecentEvents(controller: ReadableStreamDefaultController, count: number): void { try { if (existsSync(EVENTS_FILE)) { const content = readFileSync(EVENTS_FILE, 'utf-8'); const lines = content.trim().split('\n').filter(Boolean); const recentLines = lines.slice(-count); const events = recentLines .map((line) => { try { return JSON.parse(line); } catch { return null; } }) .filter(Boolean); sendEvent(controller, { type: 'events:history', data: { events }, timestamp: new Date().toISOString(), }); lastEventPosition = content.length; } } catch (error) { // Falha silenciosa } } function sendNewEvents(controller: ReadableStreamDefaultController, fromPosition: number): void { try { if (existsSync(EVENTS_FILE)) { const content = readFileSync(EVENTS_FILE, 'utf-8'); const newContent = content.slice(fromPosition); const lines = newContent.trim().split('\n').filter(Boolean); for (const line of lines) { try { const event = JSON.parse(line); sendEvent(controller, { type: 'event:new', data: event, timestamp: new Date().toISOString(), }); } catch { // Ignorar linhas malformadas } } } } catch (error) { // Falha silenciosa } } return new Response(stream, { headers: { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }, }); }

Events Store

Localização

apps/dashboard/src/stores/events-store.ts

Interface

// apps/dashboard/src/stores/events-store.ts import { create } from 'zustand'; import type { DashboardEvent } from '@/types'; // Retenção configurável (Decision #3) const DEFAULT_MAX_EVENTS = 100; export interface CurrentCommand { name: string; startedAt: string; status: 'running' | 'complete' | 'error'; errorMessage?: string; } export interface EventRetentionSettings { mode: 'session' | 'hours' | 'persistent'; hoursToKeep?: number; maxEvents?: number; } interface EventsState { // Conexão isConnected: boolean; lastUpdate: string | null; // Sessão sessionId: string | null; sessionStartedAt: string | null; // Comando atual (apenas alto nível) currentCommand: CurrentCommand | null; // Eventos recentes (apenas alto nível) recentEvents: DashboardEvent[]; // Configurações de retenção retentionSettings: EventRetentionSettings; // Ações setConnected: (connected: boolean) => void; processEvent: (event: DashboardEvent) => void; processHistoryEvents: (events: DashboardEvent[]) => void; setRetentionSettings: (settings: EventRetentionSettings) => void; clearEvents: () => void; } export const useEventsStore = create<EventsState>((set, get) => ({ // Estado inicial isConnected: false, lastUpdate: null, sessionId: null, sessionStartedAt: null, currentCommand: null, recentEvents: [], retentionSettings: { mode: 'session', hoursToKeep: 24, maxEvents: DEFAULT_MAX_EVENTS, }, // Ações setConnected: (connected) => set({ isConnected: connected }), processEvent: (event) => { const state = get(); const maxEvents = state.retentionSettings.maxEvents || DEFAULT_MAX_EVENTS; // Adicionar a eventos recentes (circular buffer) const newEvents = [...state.recentEvents, event].slice(-maxEvents); // Processar por tipo de evento (apenas alto nível) let updates: Partial<EventsState> = { recentEvents: newEvents, lastUpdate: event.timestamp, }; switch (event.type) { // Eventos de sessão case 'session:start': updates.sessionId = event.data.sessionId as string; updates.sessionStartedAt = event.timestamp; break; case 'session:end': updates.sessionId = null; updates.sessionStartedAt = null; updates.currentCommand = null; break; // Eventos de comando case 'command:start': updates.currentCommand = { name: event.data.command as string, startedAt: event.timestamp, status: 'running', }; break; case 'command:complete': if (state.currentCommand) { updates.currentCommand = { ...state.currentCommand, status: 'complete', }; // Limpar após 3 segundos setTimeout(() => { set({ currentCommand: null }); }, 3000); } break; case 'command:error': if (state.currentCommand) { updates.currentCommand = { ...state.currentCommand, status: 'error', errorMessage: event.data.error as string, }; } break; } set(updates); }, processHistoryEvents: (events) => { events.forEach((event) => { get().processEvent(event); }); }, setRetentionSettings: (settings) => { set({ retentionSettings: settings }); }, clearEvents: () => { set({ recentEvents: [], currentCommand: null, }); }, })); // Seletores export const selectCurrentCommand = (state: EventsState) => state.currentCommand; export const selectRecentEvents = (state: EventsState) => state.recentEvents; export const selectSessionInfo = (state: EventsState) => ({ sessionId: state.sessionId, startedAt: state.sessionStartedAt, });

Novos Componentes UI

Componentes Necessários (Apenas Alto Nível)

ComponenteResponsabilidadePrioridade
CommandPanelMostra comando atual e statusP0
ActivityFeedTimeline de eventos recentesP0
SessionIndicatorStatus da sessão ativaP1
RetentionSettingsConfig de retenção de eventosP2

CommandPanel

// apps/dashboard/src/components/realtime/CommandPanel.tsx 'use client'; import { useEventsStore, selectCurrentCommand } from '@/stores/events-store'; import { useEffect, useState } from 'react'; import { cn } from '@/lib/utils'; import { Loader2, CheckCircle, XCircle, Terminal } from 'lucide-react'; export function CommandPanel() { const currentCommand = useEventsStore(selectCurrentCommand); const [elapsed, setElapsed] = useState(0); useEffect(() => { if (!currentCommand || currentCommand.status !== 'running') { setElapsed(0); return; } const startTime = new Date(currentCommand.startedAt).getTime(); const interval = setInterval(() => { setElapsed(Math.floor((Date.now() - startTime) / 1000)); }, 1000); return () => clearInterval(interval); }, [currentCommand]); if (!currentCommand) { return ( <div className="p-3 rounded-lg bg-surface border border-subtle"> <div className="flex items-center gap-2 text-muted text-sm"> <Terminal className="w-4 h-4" /> <span>Aguardando comando...</span> </div> </div> ); } const formatElapsed = (seconds: number) => { const mins = Math.floor(seconds / 60); const secs = seconds % 60; return mins > 0 ? `${mins}m ${secs}s` : `${secs}s`; }; const statusConfig = { running: { icon: <Loader2 className="w-4 h-4 animate-spin text-blue-400" />, bg: 'border-blue-500/30 bg-blue-500/5', }, complete: { icon: <CheckCircle className="w-4 h-4 text-green-400" />, bg: 'border-green-500/30 bg-green-500/5', }, error: { icon: <XCircle className="w-4 h-4 text-red-400" />, bg: 'border-red-500/30 bg-red-500/5', }, }; const config = statusConfig[currentCommand.status]; return ( <div className={cn('p-3 rounded-lg border transition-luxury', config.bg)}> <div className="flex items-center justify-between"> <div className="flex items-center gap-2"> {config.icon} <span className="font-mono text-sm">{currentCommand.name}</span> </div> {currentCommand.status === 'running' && ( <span className="text-xs text-muted">{formatElapsed(elapsed)}</span> )} </div> {currentCommand.status === 'error' && currentCommand.errorMessage && ( <div className="mt-2 p-2 rounded bg-red-500/10 text-red-400 text-xs"> {currentCommand.errorMessage} </div> )} </div> ); }

ActivityFeed

// apps/dashboard/src/components/realtime/ActivityFeed.tsx 'use client'; import { useEventsStore, selectRecentEvents } from '@/stores/events-store'; import { cn } from '@/lib/utils'; import { User, Terminal, AlertCircle, Play, Square, Kanban } from 'lucide-react'; import { formatDistanceToNow } from 'date-fns'; import { ptBR } from 'date-fns/locale'; // Apenas eventos de alto nível (Decision #2) const EVENT_CONFIG: Record< string, { icon: React.ElementType; color: string; label: string } > = { 'agent:activated': { icon: User, color: 'text-purple-400', label: 'Agent ativado' }, 'agent:deactivated': { icon: User, color: 'text-gray-400', label: 'Agent desativado' }, 'command:start': { icon: Terminal, color: 'text-blue-400', label: 'Comando' }, 'command:complete': { icon: Terminal, color: 'text-green-400', label: 'Comando OK' }, 'command:error': { icon: AlertCircle, color: 'text-red-400', label: 'Erro' }, 'story:status-change': { icon: Kanban, color: 'text-orange-400', label: 'Story' }, 'session:start': { icon: Play, color: 'text-green-400', label: 'Sessão iniciada' }, 'session:end': { icon: Square, color: 'text-gray-400', label: 'Sessão encerrada' }, }; interface ActivityFeedProps { maxItems?: number; className?: string; } export function ActivityFeed({ maxItems = 15, className }: ActivityFeedProps) { const recentEvents = useEventsStore(selectRecentEvents); const displayEvents = recentEvents.slice(-maxItems).reverse(); if (displayEvents.length === 0) { return ( <div className={cn('p-4 text-center text-muted text-sm', className)}> Nenhuma atividade recente </div> ); } return ( <div className={cn('space-y-1', className)}> {displayEvents.map((event) => { const config = EVENT_CONFIG[event.type] || { icon: Terminal, color: 'text-gray-400', label: event.type, }; const Icon = config.icon; const getEventDetail = () => { switch (event.type) { case 'agent:activated': return event.data.agentName as string; case 'command:start': case 'command:complete': case 'command:error': return event.data.command as string; case 'story:status-change': return `${event.data.storyId}: ${event.data.oldStatus} → ${event.data.newStatus}`; default: return null; } }; const detail = getEventDetail(); const timeAgo = formatDistanceToNow(new Date(event.timestamp), { addSuffix: true, locale: ptBR, }); return ( <div key={event.id} className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-surface-hover transition-colors text-xs" > <Icon className={cn('w-3.5 h-3.5 flex-shrink-0', config.color)} /> <span className="text-secondary truncate flex-1"> {config.label} {detail && ( <span className="text-muted ml-1 font-mono">{detail}</span> )} </span> <span className="text-muted text-[10px] flex-shrink-0">{timeAgo}</span> </div> ); })} </div> ); }

SessionIndicator

// apps/dashboard/src/components/realtime/SessionIndicator.tsx 'use client'; import { useEventsStore, selectSessionInfo } from '@/stores/events-store'; import { formatDistanceToNow } from 'date-fns'; import { ptBR } from 'date-fns/locale'; import { Activity, Clock } from 'lucide-react'; export function SessionIndicator() { const { sessionId, startedAt } = useEventsStore(selectSessionInfo); if (!sessionId) { return ( <div className="flex items-center gap-2 text-muted text-xs"> <Activity className="w-3.5 h-3.5" /> <span>Sem sessão ativa</span> </div> ); } const duration = startedAt ? formatDistanceToNow(new Date(startedAt), { locale: ptBR }) : ''; return ( <div className="flex items-center gap-3 text-xs"> <div className="flex items-center gap-1.5 text-green-400"> <Activity className="w-3.5 h-3.5" /> <span>Sessão ativa</span> </div> {duration && ( <div className="flex items-center gap-1 text-muted"> <Clock className="w-3 h-3" /> <span>{duration}</span> </div> )} </div> ); }

Fluxo de Dados Completo (Alto Nível)

┌─────────────────────────────────────────────────────────────────────────────┐ │ COMPLETE DATA FLOW (HIGH-LEVEL ONLY) │ └─────────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────────┐ │ 1. USER ACTION IN CLI │ │ │ │ $ claude │ │ > @architect ← agent:activated │ │ 🏛️ Aria (Visionary) ready │ │ > *create-architecture ← command:start │ │ [Claude working...] │ │ ✓ Architecture created ← command:complete │ │ > *exit ← agent:deactivated │ │ │ └─────────────────────┬───────────────────────────────────────────────────────┘ │ Claude Code Hooks (Decision #1) ┌─────────────────────────────────────────────────────────────────────────────┐ │ 2. EVENTS WRITTEN TO FILESYSTEM │ │ │ │ .aios/dashboard/events.jsonl (HIGH-LEVEL ONLY) │ │ ───────────────────────────────────────────────────── │ │ {"type":"session:start","data":{"sessionId":"uuid"},"ts":"..."} │ │ {"type":"agent:activated","data":{"agentId":"architect"},"ts":"..."} │ │ {"type":"command:start","data":{"command":"*create-architecture"},"ts":""}│ │ {"type":"command:complete","data":{"success":true},"ts":"..."} │ │ {"type":"agent:deactivated","data":{"agentId":"architect"},"ts":"..."} │ │ │ └─────────────────────┬───────────────────────────────────────────────────────┘ │ File watcher ┌─────────────────────────────────────────────────────────────────────────────┐ │ 3. SSE ENDPOINT STREAMS EVENTS │ │ │ │ /api/events (Server-Sent Events) │ │ ──────────────────────────────── │ │ │ │ SSE Output (high-level events only): │ │ data: {"type":"event:new","data":{"type":"agent:activated",...}} │ │ data: {"type":"event:new","data":{"type":"command:start",...}} │ │ data: {"type":"event:new","data":{"type":"command:complete",...}} │ │ │ └─────────────────────┬───────────────────────────────────────────────────────┘ │ EventSource ┌─────────────────────────────────────────────────────────────────────────────┐ │ 4. STORES UPDATE STATE │ │ │ │ events-store (simplified) │ │ ───────────────────────── │ │ { │ │ sessionId: "uuid", │ │ currentCommand: { name: '*create-architecture', status: 'complete' }, │ │ recentEvents: [agent:activated, command:start, command:complete, ...] │ │ } │ │ │ └─────────────────────┬───────────────────────────────────────────────────────┘ │ React re-render ┌─────────────────────────────────────────────────────────────────────────────┐ │ 5. UI COMPONENTS UPDATE │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ Dashboard UI │ │ │ │ │ │ │ │ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ │ │ │ │ CommandPanel │ │ ActivityFeed │ │ │ │ │ │ ───────────────────────── │ │ ───────────────────────── │ │ │ │ │ │ │ │ │ │ │ │ │ │ ✓ *create-architecture │ │ 14:32 Comando OK │ │ │ │ │ │ Completo │ │ 14:30 Comando iniciado │ │ │ │ │ │ │ │ 14:29 Agent ativado Aria │ │ │ │ │ │ │ │ 14:28 Sessão iniciada │ │ │ │ │ └─────────────────────────────┘ └─────────────────────────────┘ │ │ │ │ │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ StatusBar │ │ │ │ │ │ ● Connected │ Sessão ativa (5 min) │ @architect (Aria) │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ └───────────────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────────┘

Implementação Faseada (Simplificada)

Fase 1: Fundação (P0)

ItemDescriçãoEsforço
Claude Code Hooks IntegrationConectar aos hooks nativos2h
events.jsonlFormato high-level1h
Enhanced SSEWatch events.jsonl2h
events-storeStore simplificado1h

Entregável: Eventos high-level fluem do CLI para o Dashboard

Fase 2: UI Core (P1)

ItemDescriçãoEsforço
CommandPanelComando atual + status1h
ActivityFeedTimeline simplificada1h
SessionIndicatorStatus da sessão30min
StatusBar integrationIntegrar novos indicadores1h

Entregável: Dashboard mostra atividade high-level em tempo real

Fase 3: Configuração (P2)

ItemDescriçãoEsforço
RetentionSettings UIToggle session/hours/persistent1h
Settings integrationPersistência de preferências1h
localStorage/IndexedDBImplementar modos de retenção2h

Entregável: Retenção de eventos configurável pelo usuário


Decisões Tomadas

1. Fonte de Eventos ✅

Decisão: Claude Code Hooks

AspectoDetalhe
ImplementaçãoUsar hooks nativos do Claude Code
VantagemAutomático, completo, sem wrapper adicional
DependênciaAPI de hooks do Claude Code

2. Nível de Detalhe ✅

Decisão: High-level apenas

Eventos IncluídosEventos Excluídos
agent:activated/deactivatedtool:call (Read/Write/Bash)
command:start/complete/errorfile:read/write/create
session:start/endllm:thinking/responding
story:status-changeOutput streaming

Rationale: Foco em monitoramento, não debug. Menor volume de dados, melhor performance.

3. Retenção de Eventos ✅

Decisão: Configurável pelo usuário

// settings-store.ts interface EventRetentionSettings { mode: 'session' | 'hours' | 'persistent'; hoursToKeep?: number; // quando mode = 'hours' maxEvents?: number; // limite máximo em qualquer modo } // Defaults const DEFAULT_RETENTION: EventRetentionSettings = { mode: 'session', hoursToKeep: 24, maxEvents: 1000, };
ModoComportamentoStorage
sessionLimpa ao fechar dashboardMemory
hoursMantém últimas N horaslocalStorage
persistentMantém até limiteIndexedDB

UI: Toggle em Settings → Events → Retention


Documentação gerada por @architect (Aria) - AIOS Core v2.0