Files
egutierrez f5b857dbc6 feat: add bubbletea TUI dashboard for bot server management
Implementa un dashboard interactivo con bubbletea siguiendo el patrón
pure core / impure shell del proyecto:

- pkg/tui/ (PURE): Model, Update, View — solo fmt y strings, cero I/O.
  Update produce Intent[] (datos puros) en vez de side effects.
- shell/tui/ (IMPURE): Adapter convierte Intent[] en tea.Cmd[] con I/O real
  (process management, /proc stats, log tail).
- cmd/dashboard/ (composición): Bridge conecta pure Update con shell Adapter
  usando la Elm Architecture de bubbletea.

Pantallas: Main Menu → Agent List → Agent Actions (start/stop/restart/kill) → Logs.
Navegación: flechas ↑↓, Enter seleccionar, 0 volver, q salir.

Dependencias añadidas: bubbletea, lipgloss.
Actualiza .gitignore para anclar binarios a raíz (/agentctl, /dashboard).
Documenta nuevos scripts en CLAUDE.md.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 19:38:30 +00:00

86 lines
1.9 KiB
Go

// Command dashboard provides an interactive TUI for managing bot agents.
//
// Usage:
//
// dashboard # launch the interactive TUI
// go run ./cmd/dashboard
package main
import (
"fmt"
"os"
tea "github.com/charmbracelet/bubbletea"
puretui "github.com/enmanuel/agents/pkg/tui"
"github.com/enmanuel/agents/shell/process"
shelltui "github.com/enmanuel/agents/shell/tui"
)
const (
runDir = "run"
agentsGlob = "agents/*/config.yaml"
)
func main() {
_ = os.MkdirAll(runDir, 0o755)
mgr := process.NewManager(runDir, agentsGlob, "")
adapter := shelltui.NewAdapter(mgr)
p := tea.NewProgram(newBridge(adapter), tea.WithAltScreen())
if _, err := p.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
// bridge implements tea.Model and connects the pure Update with the impure Adapter.
type bridge struct {
model puretui.Model
adapter *shelltui.Adapter
}
func newBridge(adapter *shelltui.Adapter) bridge {
return bridge{
model: puretui.InitialModel(),
adapter: adapter,
}
}
func (b bridge) Init() tea.Cmd {
return b.adapter.RunIntent(puretui.Intent{Kind: puretui.IntentLoadAgents})
}
func (b bridge) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
// Convert tea messages to pure messages.
var pureMsg interface{}
switch m := msg.(type) {
case tea.KeyMsg:
pureMsg = puretui.KeyMsg{Str: m.String()}
case tea.WindowSizeMsg:
pureMsg = puretui.WindowSizeMsg{Width: m.Width, Height: m.Height}
default:
// MsgAgentsLoaded, MsgActionDone, MsgLogsLoaded, MsgTick pass through.
pureMsg = msg
}
// Pure update: no side effects.
newModel, intents := puretui.Update(b.model, pureMsg)
b.model = newModel
// Convert pure intents to impure tea.Cmds.
cmds := make([]tea.Cmd, 0, len(intents))
for _, intent := range intents {
if cmd := b.adapter.RunIntent(intent); cmd != nil {
cmds = append(cmds, cmd)
}
}
return b, tea.Batch(cmds...)
}
func (b bridge) View() string {
return puretui.View(b.model)
}