// 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" "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.IntentStartAgent: return a.startAgent(intent.AgentID) case puretui.IntentStopAgent: return a.stopAgent(intent.AgentID) case puretui.IntentKillAgent: return a.killAgent(intent.AgentID) case puretui.IntentRestartAgent: return a.restartAgent(intent.AgentID) case puretui.IntentLoadLogs: return a.loadLogs(intent.AgentID) 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.StatusAll() if err != nil { return puretui.MsgAgentsLoaded{} } views := make([]puretui.AgentView, len(statuses)) for i, s := range statuses { v := puretui.AgentView{ ID: s.ID, Name: s.Name, Version: s.Version, Desc: s.Desc, Enabled: s.Enabled, Running: s.Running, PID: s.PID, } if s.Running { if stats, err := a.mgr.Stats(s.ID); err == nil { v.Uptime = formatUptime(stats.UptimeSecs) v.Memory = formatBytes(stats.MemRSSKB * 1024) v.CPU = fmt.Sprintf("%.1f%%", stats.CPUPct) v.LogSize = formatBytes(stats.LogBytes) } } views[i] = v } return puretui.MsgAgentsLoaded{Agents: views} } } func (a *Adapter) startAgent(id string) tea.Cmd { return func() tea.Msg { agents, err := a.mgr.Scan() if err != nil { return puretui.MsgActionDone{AgentID: id, Action: "Start", Err: err} } for _, agent := range agents { if agent.ID == id { err = a.mgr.Start(agent) // Give the process a moment to start. if err == nil { time.Sleep(500 * time.Millisecond) } return puretui.MsgActionDone{AgentID: id, Action: "Start", Err: err} } } return puretui.MsgActionDone{AgentID: id, Action: "Start", Err: fmt.Errorf("agent not found")} } } func (a *Adapter) stopAgent(id string) tea.Cmd { return func() tea.Msg { err := a.mgr.Stop(id) return puretui.MsgActionDone{AgentID: id, Action: "Stop", Err: err} } } func (a *Adapter) killAgent(id string) tea.Cmd { return func() tea.Msg { err := a.mgr.Kill(id) return puretui.MsgActionDone{AgentID: id, Action: "Kill", Err: err} } } func (a *Adapter) restartAgent(id string) tea.Cmd { return func() tea.Msg { // Stop first (ignore error if not running) _ = a.mgr.Stop(id) time.Sleep(300 * time.Millisecond) agents, err := a.mgr.Scan() if err != nil { return puretui.MsgActionDone{AgentID: id, Action: "Restart", Err: err} } for _, agent := range agents { if agent.ID == id { err = a.mgr.Start(agent) if err == nil { time.Sleep(500 * time.Millisecond) } return puretui.MsgActionDone{AgentID: id, Action: "Restart", Err: err} } } return puretui.MsgActionDone{AgentID: id, Action: "Restart", Err: fmt.Errorf("agent not found")} } } func (a *Adapter) loadLogs(id string) tea.Cmd { return func() tea.Msg { lines, err := a.mgr.LogTail(id, 100) if err != nil { return puretui.MsgLogsLoaded{Lines: []string{"Error: " + err.Error()}} } return puretui.MsgLogsLoaded{Lines: 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) } }