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:
+19
-14
@@ -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
@@ -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
@@ -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
@@ -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 != "" {
|
||||
|
||||
@@ -82,6 +82,8 @@ func (osProber) isAlive(pid int) bool {
|
||||
return syscall.Kill(pid, 0) == nil
|
||||
}
|
||||
|
||||
const unifiedID = "launcher" // PID/log file ID for the unified launcher
|
||||
|
||||
// Manager handles agent process lifecycle.
|
||||
type Manager struct {
|
||||
runDir string
|
||||
@@ -388,6 +390,184 @@ func (m *Manager) Build() (string, error) {
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
// ── Unified launcher ─────────────────────────────────────────────────────
|
||||
// The unified launcher runs ALL enabled agents + orchestrator in a single
|
||||
// process. PID → run/launcher.pid, log → run/launcher.log.
|
||||
|
||||
// StartUnified launches the unified launcher (no -c flag → discovers all agents).
|
||||
func (m *Manager) StartUnified() error {
|
||||
if m.IsUnifiedRunning() {
|
||||
return fmt.Errorf("unified launcher is already running (PID %d)", m.readPID(unifiedID))
|
||||
}
|
||||
if err := os.MkdirAll(m.runDir, 0o755); err != nil {
|
||||
return fmt.Errorf("create run dir: %w", err)
|
||||
}
|
||||
|
||||
logFile, err := os.OpenFile(m.logPath(unifiedID), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open log: %w", err)
|
||||
}
|
||||
|
||||
bin := m.resolvedBin()
|
||||
var cmd *exec.Cmd
|
||||
if strings.HasPrefix(bin, "go run") {
|
||||
cmd = exec.Command("go", "run", "-tags", "goolm", "./cmd/launcher", "--log-level", "info")
|
||||
} else {
|
||||
cmd = exec.Command(bin, "--log-level", "info")
|
||||
}
|
||||
|
||||
cmd.Env = m.buildEnv()
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
logFile.Close()
|
||||
return fmt.Errorf("exec: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(m.pidPath(unifiedID), []byte(strconv.Itoa(cmd.Process.Pid)), 0o644); err != nil {
|
||||
return fmt.Errorf("write PID: %w", err)
|
||||
}
|
||||
|
||||
go func() { _ = cmd.Wait() }()
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopUnified stops the unified launcher process.
|
||||
func (m *Manager) StopUnified() error {
|
||||
return m.Stop(unifiedID)
|
||||
}
|
||||
|
||||
// KillUnified sends SIGKILL to the unified launcher.
|
||||
func (m *Manager) KillUnified() error {
|
||||
return m.Kill(unifiedID)
|
||||
}
|
||||
|
||||
// IsUnifiedRunning checks if the unified launcher is alive.
|
||||
func (m *Manager) IsUnifiedRunning() bool {
|
||||
pid := m.readPID(unifiedID)
|
||||
if pid > 0 && m.isAlive(pid) {
|
||||
return true
|
||||
}
|
||||
// Fallback: search for launcher running without -c flag
|
||||
pids := m.findUnifiedPIDs()
|
||||
return len(pids) > 0
|
||||
}
|
||||
|
||||
// UnifiedPID returns the PID of the running unified launcher, or 0.
|
||||
func (m *Manager) UnifiedPID() int {
|
||||
pid := m.readPID(unifiedID)
|
||||
if pid > 0 && m.isAlive(pid) {
|
||||
return pid
|
||||
}
|
||||
pids := m.findUnifiedPIDs()
|
||||
if len(pids) > 0 {
|
||||
// Repair PID file
|
||||
_ = os.WriteFile(m.pidPath(unifiedID), []byte(strconv.Itoa(pids[0])), 0o644)
|
||||
return pids[0]
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// UnifiedStats returns resource usage for the unified launcher process.
|
||||
func (m *Manager) UnifiedStats() (ProcessStats, error) {
|
||||
pid := m.UnifiedPID()
|
||||
if pid == 0 {
|
||||
return ProcessStats{}, fmt.Errorf("unified launcher is not running")
|
||||
}
|
||||
return m.statsForPID(pid, unifiedID), nil
|
||||
}
|
||||
|
||||
// UnifiedLogTail returns the last N lines of the unified launcher log.
|
||||
func (m *Manager) UnifiedLogTail(lines int) ([]string, error) {
|
||||
return m.LogTail(unifiedID, lines)
|
||||
}
|
||||
|
||||
// StatusAllUnified returns status for all agents, deriving "running" from
|
||||
// whether the unified launcher is running + the agent is enabled.
|
||||
func (m *Manager) StatusAllUnified() ([]AgentStatus, error) {
|
||||
agents, err := m.Scan()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
launcherRunning := m.IsUnifiedRunning()
|
||||
launcherPID := m.UnifiedPID()
|
||||
|
||||
statuses := make([]AgentStatus, len(agents))
|
||||
for i, a := range agents {
|
||||
running := launcherRunning && a.Enabled
|
||||
pid := 0
|
||||
instances := 0
|
||||
if running {
|
||||
pid = launcherPID
|
||||
instances = 1
|
||||
}
|
||||
statuses[i] = AgentStatus{
|
||||
AgentInfo: a,
|
||||
Running: running,
|
||||
PID: pid,
|
||||
Instances: instances,
|
||||
}
|
||||
}
|
||||
return statuses, nil
|
||||
}
|
||||
|
||||
// ToggleEnabled sets the enabled field in an agent's config.yaml.
|
||||
func (m *Manager) ToggleEnabled(id string, enabled bool) error {
|
||||
agents, err := m.Scan()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, a := range agents {
|
||||
if a.ID == id {
|
||||
return m.setEnabledInConfig(a.ConfigPath, enabled)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("agent %q not found", id)
|
||||
}
|
||||
|
||||
// setEnabledInConfig rewrites the enabled field in a config.yaml.
|
||||
func (m *Manager) setEnabledInConfig(path string, enabled bool) error {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
val := "false"
|
||||
if enabled {
|
||||
val = "true"
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for i, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(trimmed, "enabled:") {
|
||||
// Preserve indentation
|
||||
indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
|
||||
lines[i] = indent + "enabled: " + val
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0o644)
|
||||
}
|
||||
|
||||
// findUnifiedPIDs finds launcher processes running without -c flag.
|
||||
func (m *Manager) findUnifiedPIDs() []int {
|
||||
// Search for launcher processes that do NOT have -c flag
|
||||
raw := m.prober.pgrepPIDs("launcher.*--log-level")
|
||||
var pids []int
|
||||
for _, p := range raw {
|
||||
comm := m.prober.processComm(p)
|
||||
if comm == "go" {
|
||||
continue
|
||||
}
|
||||
pids = append(pids, p)
|
||||
}
|
||||
return pids
|
||||
}
|
||||
|
||||
// ── internal helpers ─────────────────────────────────────────────────────
|
||||
|
||||
func (m *Manager) pidPath(id string) string { return filepath.Join(m.runDir, id+".pid") }
|
||||
|
||||
+89
-233
@@ -30,32 +30,26 @@ func (a *Adapter) RunIntent(intent puretui.Intent) tea.Cmd {
|
||||
case puretui.IntentLoadAgents:
|
||||
return a.loadAgents()
|
||||
|
||||
case puretui.IntentStartAgent:
|
||||
return a.startAgent(intent.AgentID)
|
||||
case puretui.IntentEnableAgent:
|
||||
return a.enableAgent(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.IntentDisableAgent:
|
||||
return a.disableAgent(intent.AgentID)
|
||||
|
||||
case puretui.IntentLoadLogs:
|
||||
return a.loadLogs(intent.AgentID)
|
||||
|
||||
case puretui.IntentStartAll:
|
||||
return a.startAll()
|
||||
case puretui.IntentStartLauncher:
|
||||
return a.startLauncher()
|
||||
|
||||
case puretui.IntentStopAll:
|
||||
return a.stopAll()
|
||||
case puretui.IntentStopLauncher:
|
||||
return a.stopLauncher()
|
||||
|
||||
case puretui.IntentRestartAll:
|
||||
return a.restartAll()
|
||||
case puretui.IntentRestartLauncher:
|
||||
return a.restartLauncher()
|
||||
|
||||
case puretui.IntentKillAll:
|
||||
return a.killAll()
|
||||
case puretui.IntentKillLauncher:
|
||||
return a.killLauncher()
|
||||
|
||||
case puretui.IntentRebuildRestart:
|
||||
return a.rebuildRestart()
|
||||
@@ -73,287 +67,149 @@ func (a *Adapter) RunIntent(intent puretui.Intent) tea.Cmd {
|
||||
|
||||
func (a *Adapter) loadAgents() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
statuses, err := a.mgr.StatusAll()
|
||||
statuses, err := a.mgr.StatusAllUnified()
|
||||
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,
|
||||
Instances: s.Instances,
|
||||
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,
|
||||
}
|
||||
|
||||
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}
|
||||
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) startAgent(id string) tea.Cmd {
|
||||
func (a *Adapter) enableAgent(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")}
|
||||
err := a.mgr.ToggleEnabled(id, true)
|
||||
return puretui.MsgActionDone{AgentID: id, Action: "Enable", Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) stopAgent(id string) tea.Cmd {
|
||||
func (a *Adapter) disableAgent(id string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
err := a.mgr.Stop(id)
|
||||
return puretui.MsgActionDone{AgentID: id, Action: "Stop", Err: err}
|
||||
err := a.mgr.ToggleEnabled(id, false)
|
||||
return puretui.MsgActionDone{AgentID: id, Action: "Disable", Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) killAgent(id string) tea.Cmd {
|
||||
func (a *Adapter) startLauncher() 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) startAll() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
agents, err := a.mgr.Scan()
|
||||
if err != nil {
|
||||
return puretui.MsgServerActionDone{Action: "Start All", Errors: []string{err.Error()}, Failed: 1}
|
||||
}
|
||||
var total, failed int
|
||||
var errs []string
|
||||
for _, agent := range agents {
|
||||
if !agent.Enabled {
|
||||
continue
|
||||
}
|
||||
if a.mgr.IsRunning(agent.ID) {
|
||||
continue
|
||||
}
|
||||
total++
|
||||
if err := a.mgr.Start(agent); err != nil {
|
||||
failed++
|
||||
errs = append(errs, fmt.Sprintf("%s: %v", agent.ID, err))
|
||||
}
|
||||
}
|
||||
if total > 0 {
|
||||
err := a.mgr.StartUnified()
|
||||
if err == nil {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
return puretui.MsgServerActionDone{Action: "Start All", Total: total, Failed: failed, Errors: errs}
|
||||
return puretui.MsgServerActionDone{Action: "Start", Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) stopAll() tea.Cmd {
|
||||
func (a *Adapter) stopLauncher() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
statuses, err := a.mgr.StatusAll()
|
||||
if err != nil {
|
||||
return puretui.MsgServerActionDone{Action: "Stop All", Errors: []string{err.Error()}, Failed: 1}
|
||||
}
|
||||
var total, failed int
|
||||
var errs []string
|
||||
for _, s := range statuses {
|
||||
if !s.Running {
|
||||
continue
|
||||
}
|
||||
total++
|
||||
if err := a.mgr.Stop(s.ID); err != nil {
|
||||
failed++
|
||||
errs = append(errs, fmt.Sprintf("%s: %v", s.ID, err))
|
||||
}
|
||||
}
|
||||
return puretui.MsgServerActionDone{Action: "Stop All", Total: total, Failed: failed, Errors: errs}
|
||||
err := a.mgr.StopUnified()
|
||||
return puretui.MsgServerActionDone{Action: "Stop", Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) restartAll() tea.Cmd {
|
||||
func (a *Adapter) restartLauncher() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
agents, err := a.mgr.Scan()
|
||||
if err != nil {
|
||||
return puretui.MsgServerActionDone{Action: "Restart All", Errors: []string{err.Error()}, Failed: 1}
|
||||
}
|
||||
|
||||
// Stop all running first
|
||||
for _, agent := range agents {
|
||||
if agent.Enabled && a.mgr.IsRunning(agent.ID) {
|
||||
_ = a.mgr.Stop(agent.ID)
|
||||
}
|
||||
}
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
|
||||
// Start all enabled
|
||||
var total, failed int
|
||||
var errs []string
|
||||
for _, agent := range agents {
|
||||
if !agent.Enabled {
|
||||
continue
|
||||
}
|
||||
total++
|
||||
if err := a.mgr.Start(agent); err != nil {
|
||||
failed++
|
||||
errs = append(errs, fmt.Sprintf("%s: %v", agent.ID, err))
|
||||
}
|
||||
}
|
||||
if total > 0 {
|
||||
_ = 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 All", Total: total, Failed: failed, Errors: errs}
|
||||
return puretui.MsgServerActionDone{Action: "Restart", Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) killAll() tea.Cmd {
|
||||
func (a *Adapter) killLauncher() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
statuses, err := a.mgr.StatusAll()
|
||||
if err != nil {
|
||||
return puretui.MsgServerActionDone{Action: "Kill All", Errors: []string{err.Error()}, Failed: 1}
|
||||
}
|
||||
var total, failed int
|
||||
var errs []string
|
||||
for _, s := range statuses {
|
||||
if !s.Running {
|
||||
continue
|
||||
}
|
||||
total++
|
||||
if err := a.mgr.Kill(s.ID); err != nil {
|
||||
failed++
|
||||
errs = append(errs, fmt.Sprintf("%s: %v", s.ID, err))
|
||||
}
|
||||
}
|
||||
return puretui.MsgServerActionDone{Action: "Kill All", Total: total, Failed: failed, Errors: errs}
|
||||
err := a.mgr.KillUnified()
|
||||
return puretui.MsgServerActionDone{Action: "Kill", Err: err}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) rebuildRestart() tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
// 1. Remember which agents are currently running
|
||||
statuses, err := a.mgr.StatusAll()
|
||||
if err != nil {
|
||||
return puretui.MsgRebuildDone{Errors: []string{err.Error()}}
|
||||
}
|
||||
var wasRunning []string
|
||||
for _, s := range statuses {
|
||||
if s.Running {
|
||||
wasRunning = append(wasRunning, s.ID)
|
||||
}
|
||||
}
|
||||
wasRunning := a.mgr.IsUnifiedRunning()
|
||||
|
||||
// 2. Stop all running agents
|
||||
for _, id := range wasRunning {
|
||||
_ = a.mgr.Stop(id)
|
||||
}
|
||||
if len(wasRunning) > 0 {
|
||||
// Stop if running
|
||||
if wasRunning {
|
||||
_ = a.mgr.StopUnified()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
// 3. Build
|
||||
// Build
|
||||
buildOut, buildErr := a.mgr.Build()
|
||||
if buildErr != nil {
|
||||
// Build failed — try to restart what was running before
|
||||
for _, id := range wasRunning {
|
||||
agents, _ := a.mgr.Scan()
|
||||
for _, ag := range agents {
|
||||
if ag.ID == id {
|
||||
_ = a.mgr.Start(ag)
|
||||
break
|
||||
}
|
||||
}
|
||||
// Build failed — try to restart if was running
|
||||
if wasRunning {
|
||||
_ = a.mgr.StartUnified()
|
||||
}
|
||||
// Return last lines of build output
|
||||
lines := strings.Split(strings.TrimSpace(buildOut), "\n")
|
||||
tail := buildOut
|
||||
if len(lines) > 5 {
|
||||
tail = strings.Join(lines[len(lines)-5:], "\n")
|
||||
}
|
||||
return puretui.MsgRebuildDone{BuildLog: tail}
|
||||
return puretui.MsgRebuildDone{BuildOK: false, BuildLog: tail}
|
||||
}
|
||||
|
||||
// 4. Restart only the agents that were running before
|
||||
agents, _ := a.mgr.Scan()
|
||||
agentMap := make(map[string]process.AgentInfo)
|
||||
for _, ag := range agents {
|
||||
agentMap[ag.ID] = ag
|
||||
}
|
||||
|
||||
var restarted, failed int
|
||||
var errs []string
|
||||
for _, id := range wasRunning {
|
||||
ag, ok := agentMap[id]
|
||||
if !ok {
|
||||
failed++
|
||||
errs = append(errs, fmt.Sprintf("%s: not found after build", id))
|
||||
continue
|
||||
// Restart launcher
|
||||
started := false
|
||||
var startErr error
|
||||
if wasRunning {
|
||||
startErr = a.mgr.StartUnified()
|
||||
if startErr == nil {
|
||||
started = true
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
if err := a.mgr.Start(ag); err != nil {
|
||||
failed++
|
||||
errs = append(errs, fmt.Sprintf("%s: %v", id, err))
|
||||
} else {
|
||||
restarted++
|
||||
}
|
||||
}
|
||||
if restarted > 0 {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
return puretui.MsgRebuildDone{
|
||||
BuildOK: true,
|
||||
Restarted: restarted,
|
||||
Failed: failed,
|
||||
Errors: errs,
|
||||
BuildOK: true,
|
||||
Started: started,
|
||||
Err: startErr,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Adapter) loadLogs(id string) tea.Cmd {
|
||||
return func() tea.Msg {
|
||||
lines, err := a.mgr.LogTail(id, 100)
|
||||
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()}}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user