diff --git a/pkg/tui/model.go b/pkg/tui/model.go index 3c36aa6..417e580 100644 --- a/pkg/tui/model.go +++ b/pkg/tui/model.go @@ -96,6 +96,7 @@ func TestMenuOptions() []MenuOption { func ServerMenuOptions(running bool) []MenuOption { if running { return []MenuOption{ + {Label: "Reload All", Desc: "Hot-reload de todos los agentes (SIGHUP)"}, {Label: "Stop", Desc: "Detener el launcher"}, {Label: "Restart", Desc: "Reiniciar el launcher"}, {Label: "Kill", Desc: "SIGKILL forzado"}, @@ -115,13 +116,15 @@ func ServerMenuOptions(running bool) []MenuOption { func AgentActionOptions(enabled bool) []MenuOption { if enabled { return []MenuOption{ - {Label: "Restart", Desc: "Reiniciar launcher para aplicar cambios"}, + {Label: "Reload", Desc: "Hot-reload este agente (SIGHUP, sin interrumpir los demás)"}, + {Label: "Restart", Desc: "Reiniciar el launcher completo (todos los agentes)"}, {Label: "Disable", Desc: "Desactivar agente (requiere restart)"}, {Label: "Logs", Desc: "Ver log del launcher"}, } } return []MenuOption{ - {Label: "Restart", Desc: "Reiniciar launcher para aplicar cambios"}, + {Label: "Reload", Desc: "Hot-reload este agente (SIGHUP, sin interrumpir los demás)"}, + {Label: "Restart", Desc: "Reiniciar el launcher completo (todos los agentes)"}, {Label: "Enable", Desc: "Activar agente (requiere restart)"}, {Label: "Logs", Desc: "Ver log del launcher"}, } diff --git a/pkg/tui/update.go b/pkg/tui/update.go index ee5d794..6f86fa9 100644 --- a/pkg/tui/update.go +++ b/pkg/tui/update.go @@ -14,7 +14,9 @@ const ( // Agent-level IntentEnableAgent IntentKind = "enable_agent" IntentDisableAgent IntentKind = "disable_agent" - IntentRestartAgent IntentKind = "restart_agent" + IntentReloadAgent IntentKind = "reload_agent" // hot-reload via SIGHUP (solo este agente) + IntentReloadAll IntentKind = "reload_all" // hot-reload via SIGHUP (todos los agentes) + IntentRestartAgent IntentKind = "restart_agent" // restart completo del launcher // Unified launcher operations IntentStartLauncher IntentKind = "start_launcher" @@ -74,8 +76,10 @@ func Update(model Model, msg interface{}) (Model, []Intent) { case MsgActionDone: if m.Err != nil { model.StatusMsg = fmt.Sprintf("Error: %s %s: %v", m.Action, m.AgentID, m.Err) + } else if m.Action == "Reload" { + model.StatusMsg = fmt.Sprintf("Reload OK — %s recargado sin interrupciones", m.AgentID) } else if m.Action == "Restart" { - model.StatusMsg = fmt.Sprintf("Restart OK — all agents reloaded") + model.StatusMsg = "Restart OK — launcher reiniciado" } else { model.StatusMsg = fmt.Sprintf("%s %s OK — restart launcher to apply", m.Action, m.AgentID) } @@ -84,6 +88,8 @@ func Update(model Model, msg interface{}) (Model, []Intent) { case MsgServerActionDone: if m.Err != nil { model.StatusMsg = fmt.Sprintf("Error: %s: %v", m.Action, m.Err) + } else if m.Action == "Reload All" { + model.StatusMsg = "Reload All OK — SIGHUP enviado al launcher" } else { model.StatusMsg = fmt.Sprintf("%s OK", m.Action) } @@ -245,6 +251,9 @@ func executeAction(model Model, action string) (Model, []Intent) { case "Disable": model.StatusMsg = "Disabling " + id + "..." return model, []Intent{{Kind: IntentDisableAgent, AgentID: id}} + case "Reload": + model.StatusMsg = "Hot-reloading " + id + "..." + return model, []Intent{{Kind: IntentReloadAgent, AgentID: id}} case "Restart": model.StatusMsg = "Restarting launcher (all agents)..." return model, []Intent{{Kind: IntentRestartAgent, AgentID: id}} @@ -302,6 +311,9 @@ func updateServerScreen(model Model, key KeyMsg) (Model, []Intent) { func executeServerAction(model Model, action string) (Model, []Intent) { switch action { + case "Reload All": + model.StatusMsg = "Hot-reloading all agents..." + return model, []Intent{{Kind: IntentReloadAll}} case "Start": model.StatusMsg = "Starting launcher..." return model, []Intent{{Kind: IntentStartLauncher}} diff --git a/shell/tui/adapter.go b/shell/tui/adapter.go index 72264e3..e177659 100644 --- a/shell/tui/adapter.go +++ b/shell/tui/adapter.go @@ -39,6 +39,12 @@ func (a *Adapter) RunIntent(intent puretui.Intent) tea.Cmd { case puretui.IntentDisableAgent: return a.disableAgent(intent.AgentID) + case puretui.IntentReloadAgent: + return a.reloadAgent(intent.AgentID) + + case puretui.IntentReloadAll: + return a.reloadAll() + case puretui.IntentRestartAgent: return a.restartAgent(intent.AgentID) @@ -140,30 +146,57 @@ func (a *Adapter) disableAgent(id string) tea.Cmd { } } -func (a *Adapter) restartAgent(id string) tea.Cmd { +// reloadAgent hot-reloads a single agent via SIGHUP without stopping the launcher. +func (a *Adapter) reloadAgent(id string) tea.Cmd { return func() tea.Msg { pid := a.mgr.UnifiedPID() if pid <= 0 { - // Launcher not running — fall back to full restart. - _ = 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} + return puretui.MsgActionDone{AgentID: id, Action: "Reload", + Err: fmt.Errorf("el launcher no está corriendo")} } - - // Launcher is running — write target and send SIGHUP for hot-reload. if id != "" { - _ = os.WriteFile("run/reload.txt", []byte(id), 0o644) + if err := os.WriteFile("run/reload.txt", []byte(id), 0o644); err != nil { + return puretui.MsgActionDone{AgentID: id, Action: "Reload", Err: err} + } } err := syscall.Kill(pid, syscall.SIGHUP) if err != nil { - return puretui.MsgActionDone{AgentID: id, Action: "Restart", Err: err} + return puretui.MsgActionDone{AgentID: id, Action: "Reload", Err: err} } time.Sleep(1 * time.Second) - return puretui.MsgActionDone{AgentID: id, Action: "Restart", Err: nil} + return puretui.MsgActionDone{AgentID: id, Action: "Reload", Err: nil} + } +} + +// reloadAll hot-reloads all agents via SIGHUP (no reload.txt → reload all). +func (a *Adapter) reloadAll() tea.Cmd { + return func() tea.Msg { + pid := a.mgr.UnifiedPID() + if pid <= 0 { + return puretui.MsgServerActionDone{Action: "Reload All", + Err: fmt.Errorf("el launcher no está corriendo")} + } + // Remove stale reload.txt so the launcher reloads all agents. + _ = os.Remove("run/reload.txt") + err := syscall.Kill(pid, syscall.SIGHUP) + if err != nil { + return puretui.MsgServerActionDone{Action: "Reload All", Err: err} + } + time.Sleep(1 * time.Second) + return puretui.MsgServerActionDone{Action: "Reload All", Err: nil} + } +} + +// restartAgent stops and restarts the whole launcher (full restart, all agents). +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} } }