feat: update dashboard and process manager for unified launcher

Actualiza el dashboard TUI y el process manager para el modelo de launcher
unificado donde todos los agentes corren en un solo proceso.

Dashboard (pkg/tui):
- model.go: campos de estado del launcher (PID, uptime, memory, CPU, log size)
- model.go: ServerMenuOptions(running) contextual, AgentActionOptions(enabled)
- messages.go: MsgAgentsLoaded incluye estado del launcher, MsgServerActionDone/MsgRebuildDone simplificados
- update.go: intents nuevos (Enable/Disable agent, Start/Stop/Restart/Kill launcher)
- view.go: vista de servidor muestra stats del launcher, agentes muestran enabled/disabled

Shell adapter (shell/tui):
- adapter.go: reescrito para usar métodos unificados (StartUnified, StopUnified, ToggleEnabled, StatusAllUnified, UnifiedStats, UnifiedLogTail)

Process manager (shell/process):
- manager.go: métodos StartUnified, StopUnified, KillUnified, IsUnifiedRunning, UnifiedPID, UnifiedStats, UnifiedLogTail, StatusAllUnified, ToggleEnabled

Los agentes ya no se inician/detienen individualmente desde el dashboard.
Se habilitan/deshabilitan en config y se reinicia el launcher para aplicar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 09:05:57 +00:00
parent 2667af52cc
commit 1af0457c1f
6 changed files with 432 additions and 370 deletions
+19 -14
View File
@@ -3,34 +3,39 @@ package tui
// Messages are pure data returned by the shell adapter.
// They carry the result of an I/O operation back into the pure Update.
// MsgAgentsLoaded carries refreshed agent data.
type MsgAgentsLoaded struct{ Agents []AgentView }
// MsgAgentsLoaded carries refreshed agent data + launcher status.
type MsgAgentsLoaded struct {
Agents []AgentView
LauncherRunning bool
LauncherPID int
LauncherUptime string
LauncherMemory string
LauncherCPU string
LauncherLogSize string
}
// MsgActionDone reports the result of an agent action (start/stop/kill/restart).
// MsgActionDone reports the result of an action (start/stop/enable/disable).
type MsgActionDone struct {
AgentID string
Action string
Err error
}
// MsgLogsLoaded carries log lines for the selected agent.
// MsgLogsLoaded carries log lines for display.
type MsgLogsLoaded struct{ Lines []string }
// MsgServerActionDone reports the result of a server-wide bulk action.
// MsgServerActionDone reports the result of a launcher action.
type MsgServerActionDone struct {
Action string
Total int
Failed int
Errors []string
Action string
Err error
}
// MsgRebuildDone reports the result of a rebuild & restart cycle.
type MsgRebuildDone struct {
BuildOK bool
BuildLog string // last lines of build output
Restarted int // agents restarted after build
Failed int
Errors []string
BuildOK bool
BuildLog string // last lines of build output
Started bool // launcher started after build
Err error
}
// MsgTick triggers a periodic refresh.
+34 -23
View File
@@ -24,17 +24,25 @@ type Model struct {
StatusMsg string // flash message ("Started OK", "Error: ...")
WindowWidth int
WindowHeight int
// Unified launcher state
LauncherRunning bool
LauncherPID int
LauncherUptime string
LauncherMemory string
LauncherCPU string
LauncherLogSize string
}
// AgentView is a pre-formatted projection of an agent for display.
type AgentView struct {
ID string
Name string
Version string
Desc string
Enabled bool
Running bool
PID int
ID string
Name string
Version string
Desc string
Enabled bool
Running bool
PID int
Instances int // number of running instances (>1 means duplicates)
Uptime string // formatted: "2h 15m"
Memory string // formatted: "42 MB"
@@ -52,36 +60,39 @@ type MenuOption struct {
func MainMenuOptions() []MenuOption {
return []MenuOption{
{Label: "Agents", Desc: "Gestionar agentes"},
{Label: "Server", Desc: "Gestionar servidor"},
{Label: "Server", Desc: "Gestionar launcher unificado"},
{Label: "Quit", Desc: "Salir"},
}
}
// ServerMenuOptions returns the available server-wide actions.
func ServerMenuOptions() []MenuOption {
func ServerMenuOptions(running bool) []MenuOption {
if running {
return []MenuOption{
{Label: "Stop", Desc: "Detener el launcher"},
{Label: "Restart", Desc: "Reiniciar el launcher"},
{Label: "Kill", Desc: "SIGKILL forzado"},
{Label: "Rebuild & Restart", Desc: "Build + reiniciar"},
{Label: "Logs", Desc: "Ver log del launcher"},
}
}
return []MenuOption{
{Label: "Start All", Desc: "Iniciar todos los agentes habilitados"},
{Label: "Stop All", Desc: "Detener todos los agentes"},
{Label: "Restart All", Desc: "Reiniciar todos los agentes"},
{Label: "Kill All", Desc: "SIGKILL forzado a todos"},
{Label: "Rebuild & Restart", Desc: "Build + reiniciar activos"},
{Label: "Start", Desc: "Iniciar el launcher unificado"},
{Label: "Rebuild & Restart", Desc: "Build + iniciar"},
}
}
// AgentActionOptions returns the available actions based on agent state.
func AgentActionOptions(running bool) []MenuOption {
if running {
func AgentActionOptions(enabled bool) []MenuOption {
if enabled {
return []MenuOption{
{Label: "Start", Desc: "Iniciar otra instancia"},
{Label: "Stop", Desc: "Detener todas las instancias"},
{Label: "Restart", Desc: "Reiniciar (stop all + start)"},
{Label: "Kill", Desc: "SIGKILL forzado a todas"},
{Label: "Logs", Desc: "Ver log del agente"},
{Label: "Disable", Desc: "Desactivar agente (requiere restart)"},
{Label: "Logs", Desc: "Ver log del launcher"},
}
}
return []MenuOption{
{Label: "Start", Desc: "Iniciar el agente"},
{Label: "Logs", Desc: "Ver log del agente"},
{Label: "Enable", Desc: "Activar agente (requiere restart)"},
{Label: "Logs", Desc: "Ver log del launcher"},
}
}
+68 -59
View File
@@ -6,20 +6,20 @@ import "fmt"
type IntentKind string
const (
IntentLoadAgents IntentKind = "load_agents"
IntentStartAgent IntentKind = "start_agent"
IntentStopAgent IntentKind = "stop_agent"
IntentKillAgent IntentKind = "kill_agent"
IntentRestartAgent IntentKind = "restart_agent"
IntentLoadLogs IntentKind = "load_logs"
IntentTick IntentKind = "tick"
IntentQuit IntentKind = "quit"
IntentLoadAgents IntentKind = "load_agents"
IntentLoadLogs IntentKind = "load_logs"
IntentTick IntentKind = "tick"
IntentQuit IntentKind = "quit"
// Server-wide bulk operations
IntentStartAll IntentKind = "start_all"
IntentStopAll IntentKind = "stop_all"
IntentRestartAll IntentKind = "restart_all"
IntentKillAll IntentKind = "kill_all"
// Agent-level
IntentEnableAgent IntentKind = "enable_agent"
IntentDisableAgent IntentKind = "disable_agent"
// Unified launcher operations
IntentStartLauncher IntentKind = "start_launcher"
IntentStopLauncher IntentKind = "stop_launcher"
IntentRestartLauncher IntentKind = "restart_launcher"
IntentKillLauncher IntentKind = "kill_launcher"
IntentRebuildRestart IntentKind = "rebuild_restart"
)
@@ -32,7 +32,7 @@ type Intent struct {
// KeyMsg is the pure representation of a key press.
// The bridge layer converts tea.KeyMsg into this.
type KeyMsg struct {
Str string // "up", "down", "enter", "0", "q", "r", etc.
Str string // "up", "down", "enter", "0", "q", "r", etc.
}
// WindowSizeMsg carries terminal dimensions.
@@ -52,7 +52,12 @@ func Update(model Model, msg interface{}) (Model, []Intent) {
case MsgAgentsLoaded:
model.Agents = m.Agents
// Clamp cursor only on screens that use the agent list
model.LauncherRunning = m.LauncherRunning
model.LauncherPID = m.LauncherPID
model.LauncherUptime = m.LauncherUptime
model.LauncherMemory = m.LauncherMemory
model.LauncherCPU = m.LauncherCPU
model.LauncherLogSize = m.LauncherLogSize
if model.Screen == ScreenAgentList {
if model.Cursor >= len(model.Agents) && len(model.Agents) > 0 {
model.Cursor = len(model.Agents) - 1
@@ -64,25 +69,27 @@ func Update(model Model, msg interface{}) (Model, []Intent) {
if m.Err != nil {
model.StatusMsg = fmt.Sprintf("Error: %s %s: %v", m.Action, m.AgentID, m.Err)
} else {
model.StatusMsg = fmt.Sprintf("%s %s OK", m.Action, m.AgentID)
model.StatusMsg = fmt.Sprintf("%s %s OK — restart launcher to apply", m.Action, m.AgentID)
}
return model, []Intent{{Kind: IntentLoadAgents}}
case MsgServerActionDone:
if m.Err != nil {
model.StatusMsg = fmt.Sprintf("Error: %s: %v", m.Action, m.Err)
} else {
model.StatusMsg = fmt.Sprintf("%s OK", m.Action)
}
return model, []Intent{{Kind: IntentLoadAgents}}
case MsgRebuildDone:
if !m.BuildOK {
model.StatusMsg = fmt.Sprintf("Build failed: %s", m.BuildLog)
} else if m.Failed > 0 {
model.StatusMsg = fmt.Sprintf("Built OK, %d/%d agents failed to restart", m.Failed, m.Restarted+m.Failed)
} else if m.Err != nil {
model.StatusMsg = fmt.Sprintf("Built OK, start failed: %v", m.Err)
} else if m.Started {
model.StatusMsg = "Built OK, launcher started"
} else {
model.StatusMsg = fmt.Sprintf("Built OK, %d agents restarted", m.Restarted)
}
return model, []Intent{{Kind: IntentLoadAgents}}
case MsgServerActionDone:
if m.Failed == 0 {
model.StatusMsg = fmt.Sprintf("%s: %d agents OK", m.Action, m.Total)
} else {
model.StatusMsg = fmt.Sprintf("%s: %d/%d failed", m.Action, m.Failed, m.Total)
model.StatusMsg = "Built OK"
}
return model, []Intent{{Kind: IntentLoadAgents}}
@@ -102,7 +109,6 @@ func Update(model Model, msg interface{}) (Model, []Intent) {
}
func updateKey(model Model, key KeyMsg) (Model, []Intent) {
// Global quit
if key.Str == "q" && model.Screen == ScreenMain {
return model, []Intent{{Kind: IntentQuit}}
}
@@ -178,7 +184,7 @@ func updateAgentActions(model Model, key KeyMsg) (Model, []Intent) {
return model, nil
}
opts := AgentActionOptions(model.Selected.Running)
opts := AgentActionOptions(model.Selected.Enabled)
switch key.Str {
case "0":
@@ -202,18 +208,12 @@ func updateAgentActions(model Model, key KeyMsg) (Model, []Intent) {
func executeAction(model Model, action string) (Model, []Intent) {
id := model.Selected.ID
switch action {
case "Start":
model.StatusMsg = "Starting " + id + "..."
return model, []Intent{{Kind: IntentStartAgent, AgentID: id}}
case "Stop":
model.StatusMsg = "Stopping " + id + "..."
return model, []Intent{{Kind: IntentStopAgent, AgentID: id}}
case "Restart":
model.StatusMsg = "Restarting " + id + "..."
return model, []Intent{{Kind: IntentRestartAgent, AgentID: id}}
case "Kill":
model.StatusMsg = "Killing " + id + "..."
return model, []Intent{{Kind: IntentKillAgent, AgentID: id}}
case "Enable":
model.StatusMsg = "Enabling " + id + "..."
return model, []Intent{{Kind: IntentEnableAgent, AgentID: id}}
case "Disable":
model.StatusMsg = "Disabling " + id + "..."
return model, []Intent{{Kind: IntentDisableAgent, AgentID: id}}
case "Logs":
model.Screen = ScreenLogs
model.LogLines = nil
@@ -227,7 +227,11 @@ func executeAction(model Model, action string) (Model, []Intent) {
func updateLogs(model Model, key KeyMsg) (Model, []Intent) {
switch key.Str {
case "0":
model.Screen = ScreenAgentActions
if model.Selected != nil {
model.Screen = ScreenAgentActions
} else {
model.Screen = ScreenServer
}
model.Cursor = 0
model.LogLines = nil
model.LogScroll = 0
@@ -237,15 +241,13 @@ func updateLogs(model Model, key KeyMsg) (Model, []Intent) {
maxScroll := max(0, len(model.LogLines)-visibleLogLines(model))
model.LogScroll = min(model.LogScroll+1, maxScroll)
case "r":
if model.Selected != nil {
return model, []Intent{{Kind: IntentLoadLogs, AgentID: model.Selected.ID}}
}
return model, []Intent{{Kind: IntentLoadLogs}}
}
return model, nil
}
func updateServerScreen(model Model, key KeyMsg) (Model, []Intent) {
opts := ServerMenuOptions()
opts := ServerMenuOptions(model.LauncherRunning)
switch key.Str {
case "0":
@@ -266,21 +268,28 @@ func updateServerScreen(model Model, key KeyMsg) (Model, []Intent) {
func executeServerAction(model Model, action string) (Model, []Intent) {
switch action {
case "Start All":
model.StatusMsg = "Starting all agents..."
return model, []Intent{{Kind: IntentStartAll}}
case "Stop All":
model.StatusMsg = "Stopping all agents..."
return model, []Intent{{Kind: IntentStopAll}}
case "Restart All":
model.StatusMsg = "Restarting all agents..."
return model, []Intent{{Kind: IntentRestartAll}}
case "Kill All":
model.StatusMsg = "Killing all agents..."
return model, []Intent{{Kind: IntentKillAll}}
case "Start":
model.StatusMsg = "Starting launcher..."
return model, []Intent{{Kind: IntentStartLauncher}}
case "Stop":
model.StatusMsg = "Stopping launcher..."
return model, []Intent{{Kind: IntentStopLauncher}}
case "Restart":
model.StatusMsg = "Restarting launcher..."
return model, []Intent{{Kind: IntentRestartLauncher}}
case "Kill":
model.StatusMsg = "Killing launcher..."
return model, []Intent{{Kind: IntentKillLauncher}}
case "Rebuild & Restart":
model.StatusMsg = "Building & restarting..."
return model, []Intent{{Kind: IntentRebuildRestart}}
case "Logs":
model.Screen = ScreenLogs
model.LogLines = nil
model.LogScroll = 0
model.Selected = nil
model.Cursor = 0
return model, []Intent{{Kind: IntentLoadLogs}}
}
return model, nil
}
@@ -288,7 +297,7 @@ func executeServerAction(model Model, action string) (Model, []Intent) {
// ── pure helpers ─────────────────────────────────────────────────────────
func visibleLogLines(m Model) int {
lines := m.WindowHeight - 6 // header + footer
lines := m.WindowHeight - 6
if lines < 5 {
return 5
}
+42 -41
View File
@@ -102,39 +102,26 @@ func viewAgentActions(m Model) string {
}
a := m.Selected
icon := "○ stopped"
if a.Running {
if a.Instances > 1 {
icon = fmt.Sprintf("● running %d instances", a.Instances)
} else {
icon = fmt.Sprintf("● running PID %d", a.PID)
}
var icon string
switch {
case !a.Enabled:
icon = " disabled"
case a.Running:
icon = "● enabled (running)"
default:
icon = "○ enabled (stopped)"
}
b.WriteString(fmt.Sprintf("\n %s %s\n", a.ID, icon))
b.WriteString(" " + strings.Repeat("─", 44) + "\n")
// Stats line if running
if a.Running && (a.Memory != "" || a.CPU != "") {
parts := []string{}
if a.Uptime != "" {
parts = append(parts, "uptime: "+a.Uptime)
}
if a.Memory != "" {
parts = append(parts, "mem: "+a.Memory)
}
if a.CPU != "" {
parts = append(parts, "cpu: "+a.CPU)
}
if a.LogSize != "" {
parts = append(parts, "log: "+a.LogSize)
}
b.WriteString(" " + strings.Join(parts, " ") + "\n")
if a.Desc != "" {
b.WriteString(" " + a.Desc + "\n")
}
b.WriteString("\n")
opts := AgentActionOptions(a.Running)
opts := AgentActionOptions(a.Enabled)
for i, opt := range opts {
cursor := " "
if i == m.Cursor {
@@ -154,7 +141,7 @@ func viewAgentActions(m Model) string {
func viewLogs(m Model) string {
var b strings.Builder
agentID := "?"
agentID := "Launcher"
if m.Selected != nil {
agentID = m.Selected.ID
}
@@ -190,27 +177,41 @@ func viewLogs(m Model) string {
func viewServer(m Model) string {
var b strings.Builder
b.WriteString("\n Server Management\n")
b.WriteString("\n Launcher Management\n")
b.WriteString(" " + strings.Repeat("─", 44) + "\n")
// Summary
running, stopped, disabled := countStatuses(m.Agents)
total := len(m.Agents)
if total > 0 {
b.WriteString(fmt.Sprintf(" %d agents: %d running, %d stopped, %d disabled\n", total, running, stopped, disabled))
// Launcher status
if m.LauncherRunning {
b.WriteString(fmt.Sprintf(" ● Launcher running PID %d\n", m.LauncherPID))
parts := []string{}
if m.LauncherUptime != "" {
parts = append(parts, "uptime: "+m.LauncherUptime)
}
if m.LauncherMemory != "" {
parts = append(parts, "mem: "+m.LauncherMemory)
}
if m.LauncherCPU != "" {
parts = append(parts, "cpu: "+m.LauncherCPU)
}
if m.LauncherLogSize != "" {
parts = append(parts, "log: "+m.LauncherLogSize)
}
if len(parts) > 0 {
b.WriteString(" " + strings.Join(parts, " ") + "\n")
}
} else {
b.WriteString(" Loading...\n")
b.WriteString(" ○ Launcher stopped\n")
}
// Agent status list (compact)
if total > 0 {
b.WriteString("\n")
// Agent summary
_, _, disabled := countStatuses(m.Agents)
enabled := len(m.Agents) - disabled
if len(m.Agents) > 0 {
b.WriteString(fmt.Sprintf("\n %d agents (%d enabled, %d disabled)\n", len(m.Agents), enabled, disabled))
for _, a := range m.Agents {
icon := ""
icon := ""
if !a.Enabled {
icon = " "
} else if a.Running {
icon = "●"
icon = ""
}
b.WriteString(fmt.Sprintf(" %s %s\n", icon, a.ID))
}
@@ -219,12 +220,12 @@ func viewServer(m Model) string {
b.WriteString("\n")
// Action menu
for i, opt := range ServerMenuOptions() {
for i, opt := range ServerMenuOptions(m.LauncherRunning) {
cursor := " "
if i == m.Cursor {
cursor = "> "
}
b.WriteString(fmt.Sprintf(" %s%-16s %s\n", cursor, opt.Label, opt.Desc))
b.WriteString(fmt.Sprintf(" %s%-20s %s\n", cursor, opt.Label, opt.Desc))
}
if m.StatusMsg != "" {