Files
agents_and_robots/shell/tui/adapter.go
T
egutierrez 525425a81c feat: opción Restart en TUI dashboard de agentes
Añade botón "Restart" en el menú de acciones de agente en la TUI.
Ejecuta stop + start del launcher unificado para aplicar cambios
de configuración sin salir del dashboard. Incluye intent nuevo
IntentRestartAgent y su implementación en el adapter impuro.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 15:46:23 +00:00

297 lines
6.8 KiB
Go

// Package tui is the impure shell layer for the TUI.
// It converts pure Intent values into real I/O via tea.Cmd.
package tui
import (
"fmt"
"os/exec"
"strings"
"time"
tea "github.com/charmbracelet/bubbletea"
puretui "github.com/enmanuel/agents/pkg/tui"
"github.com/enmanuel/agents/shell/process"
)
// Adapter bridges pure Intents with the process Manager.
type Adapter struct {
mgr *process.Manager
}
// NewAdapter creates an Adapter with the given Manager.
func NewAdapter(mgr *process.Manager) *Adapter {
return &Adapter{mgr: mgr}
}
// RunIntent converts a pure Intent into a bubbletea Cmd that performs I/O.
func (a *Adapter) RunIntent(intent puretui.Intent) tea.Cmd {
switch intent.Kind {
case puretui.IntentLoadAgents:
return a.loadAgents()
case puretui.IntentEnableAgent:
return a.enableAgent(intent.AgentID)
case puretui.IntentDisableAgent:
return a.disableAgent(intent.AgentID)
case puretui.IntentRestartAgent:
return a.restartAgent(intent.AgentID)
case puretui.IntentLoadLogs:
return a.loadLogs(intent.AgentID)
case puretui.IntentStartLauncher:
return a.startLauncher()
case puretui.IntentStopLauncher:
return a.stopLauncher()
case puretui.IntentRestartLauncher:
return a.restartLauncher()
case puretui.IntentKillLauncher:
return a.killLauncher()
case puretui.IntentRebuildRestart:
return a.rebuildRestart()
case puretui.IntentRunTests:
return a.runTests()
case puretui.IntentTick:
return a.tick()
case puretui.IntentQuit:
return tea.Quit
default:
return nil
}
}
func (a *Adapter) loadAgents() tea.Cmd {
return func() tea.Msg {
statuses, err := a.mgr.StatusAllUnified()
if err != nil {
return puretui.MsgAgentsLoaded{}
}
views := make([]puretui.AgentView, len(statuses))
for i, s := range statuses {
views[i] = puretui.AgentView{
ID: s.ID,
Name: s.Name,
Version: s.Version,
Desc: s.Desc,
Enabled: s.Enabled,
Running: s.Running,
PID: s.PID,
}
}
msg := puretui.MsgAgentsLoaded{
Agents: views,
LauncherRunning: a.mgr.IsUnifiedRunning(),
LauncherPID: a.mgr.UnifiedPID(),
}
// Launcher stats
if msg.LauncherRunning {
if stats, err := a.mgr.UnifiedStats(); err == nil {
msg.LauncherUptime = formatUptime(stats.UptimeSecs)
msg.LauncherMemory = formatBytes(stats.MemRSSKB * 1024)
msg.LauncherCPU = fmt.Sprintf("%.1f%%", stats.CPUPct)
msg.LauncherLogSize = formatBytes(stats.LogBytes)
}
}
return msg
}
}
func (a *Adapter) enableAgent(id string) tea.Cmd {
return func() tea.Msg {
err := a.mgr.ToggleEnabled(id, true)
return puretui.MsgActionDone{AgentID: id, Action: "Enable", Err: err}
}
}
func (a *Adapter) disableAgent(id string) tea.Cmd {
return func() tea.Msg {
err := a.mgr.ToggleEnabled(id, false)
return puretui.MsgActionDone{AgentID: id, Action: "Disable", Err: err}
}
}
func (a *Adapter) restartAgent(id string) tea.Cmd {
return func() tea.Msg {
_ = a.mgr.StopUnified()
time.Sleep(500 * time.Millisecond)
err := a.mgr.StartUnified()
if err == nil {
time.Sleep(500 * time.Millisecond)
}
return puretui.MsgActionDone{AgentID: id, Action: "Restart", Err: err}
}
}
func (a *Adapter) startLauncher() tea.Cmd {
return func() tea.Msg {
err := a.mgr.StartUnified()
if err == nil {
time.Sleep(500 * time.Millisecond)
}
return puretui.MsgServerActionDone{Action: "Start", Err: err}
}
}
func (a *Adapter) stopLauncher() tea.Cmd {
return func() tea.Msg {
err := a.mgr.StopUnified()
return puretui.MsgServerActionDone{Action: "Stop", Err: err}
}
}
func (a *Adapter) restartLauncher() tea.Cmd {
return func() tea.Msg {
_ = a.mgr.StopUnified()
time.Sleep(500 * time.Millisecond)
err := a.mgr.StartUnified()
if err == nil {
time.Sleep(500 * time.Millisecond)
}
return puretui.MsgServerActionDone{Action: "Restart", Err: err}
}
}
func (a *Adapter) killLauncher() tea.Cmd {
return func() tea.Msg {
err := a.mgr.KillUnified()
return puretui.MsgServerActionDone{Action: "Kill", Err: err}
}
}
func (a *Adapter) rebuildRestart() tea.Cmd {
return func() tea.Msg {
wasRunning := a.mgr.IsUnifiedRunning()
// Stop if running
if wasRunning {
_ = a.mgr.StopUnified()
time.Sleep(500 * time.Millisecond)
}
// Build
buildOut, buildErr := a.mgr.Build()
if buildErr != nil {
// Build failed — try to restart if was running
if wasRunning {
_ = a.mgr.StartUnified()
}
lines := strings.Split(strings.TrimSpace(buildOut), "\n")
tail := buildOut
if len(lines) > 5 {
tail = strings.Join(lines[len(lines)-5:], "\n")
}
return puretui.MsgRebuildDone{BuildOK: false, BuildLog: tail}
}
// Restart launcher
started := false
var startErr error
if wasRunning {
startErr = a.mgr.StartUnified()
if startErr == nil {
started = true
time.Sleep(500 * time.Millisecond)
}
}
return puretui.MsgRebuildDone{
BuildOK: true,
Started: started,
Err: startErr,
}
}
}
func (a *Adapter) loadLogs(id string) tea.Cmd {
return func() tea.Msg {
var lines []string
var err error
if id == "" {
// Launcher logs
lines, err = a.mgr.UnifiedLogTail(100)
} else {
// Agent logs — in unified mode, all go to launcher log
lines, err = a.mgr.UnifiedLogTail(100)
}
if err != nil {
return puretui.MsgLogsLoaded{Lines: []string{"Error: " + err.Error()}}
}
return puretui.MsgLogsLoaded{Lines: lines}
}
}
func (a *Adapter) runTests() tea.Cmd {
return func() tea.Msg {
goBin, err := exec.LookPath("go")
if err != nil {
// Fallback to known location
goBin = "/usr/local/go/bin/go"
}
cmd := exec.Command(goBin, "test", "-tags", "goolm", "-count=1", "./...")
cmd.Env = a.mgr.BuildEnv()
out, err := cmd.CombinedOutput()
output := strings.TrimSpace(string(out))
if output == "" && err != nil {
output = "Error: " + err.Error()
}
lines := strings.Split(output, "\n")
passed := err == nil
return puretui.MsgTestsDone{Passed: passed, Output: lines}
}
}
func (a *Adapter) tick() tea.Cmd {
return tea.Tick(3*time.Second, func(time.Time) tea.Msg {
return puretui.MsgTick{}
})
}
// ── formatting helpers ───────────────────────────────────────────────────
func formatUptime(secs int64) string {
if secs < 0 {
return "n/a"
}
d := secs / 86400
h := (secs % 86400) / 3600
m := (secs % 3600) / 60
if d > 0 {
return fmt.Sprintf("%dd %dh", d, h)
}
if h > 0 {
return fmt.Sprintf("%dh %dm", h, m)
}
return fmt.Sprintf("%dm", m)
}
func formatBytes(bytes int64) string {
switch {
case bytes >= 1<<30:
return fmt.Sprintf("%.1f GB", float64(bytes)/float64(1<<30))
case bytes >= 1<<20:
return fmt.Sprintf("%.1f MB", float64(bytes)/float64(1<<20))
case bytes >= 1<<10:
return fmt.Sprintf("%.1f KB", float64(bytes)/float64(1<<10))
default:
return fmt.Sprintf("%d B", bytes)
}
}