package tui import "fmt" // IntentKind represents a side effect the shell must perform. type IntentKind string const ( IntentLoadAgents IntentKind = "load_agents" IntentLoadLogs IntentKind = "load_logs" IntentTick IntentKind = "tick" IntentQuit IntentKind = "quit" // 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" ) // Intent is pure data describing a side effect to execute. type Intent struct { Kind IntentKind AgentID string } // 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. } // WindowSizeMsg carries terminal dimensions. type WindowSizeMsg struct { Width int Height int } // Update is PURE: (Model, msg) → (Model, []Intent). No side effects. func Update(model Model, msg interface{}) (Model, []Intent) { switch m := msg.(type) { case WindowSizeMsg: model.WindowWidth = m.Width model.WindowHeight = m.Height return model, nil case MsgAgentsLoaded: model.Agents = m.Agents 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 } } return model, []Intent{{Kind: IntentTick}} case MsgActionDone: 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 — 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.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 = "Built OK" } return model, []Intent{{Kind: IntentLoadAgents}} case MsgLogsLoaded: model.LogLines = m.Lines model.LogScroll = max(0, len(m.Lines)-visibleLogLines(model)) return model, nil case MsgTick: return model, []Intent{{Kind: IntentLoadAgents}} case KeyMsg: return updateKey(model, m) } return model, nil } func updateKey(model Model, key KeyMsg) (Model, []Intent) { if key.Str == "q" && model.Screen == ScreenMain { return model, []Intent{{Kind: IntentQuit}} } if key.Str == "ctrl+c" { return model, []Intent{{Kind: IntentQuit}} } switch model.Screen { case ScreenMain: return updateMainScreen(model, key) case ScreenAgentList: return updateAgentList(model, key) case ScreenAgentActions: return updateAgentActions(model, key) case ScreenLogs: return updateLogs(model, key) case ScreenServer: return updateServerScreen(model, key) } return model, nil } func updateMainScreen(model Model, key KeyMsg) (Model, []Intent) { opts := MainMenuOptions() switch key.Str { case "up", "k": model.Cursor = clamp(model.Cursor-1, 0, len(opts)-1) case "down", "j": model.Cursor = clamp(model.Cursor+1, 0, len(opts)-1) case "enter": switch opts[model.Cursor].Label { case "Agents": model.Screen = ScreenAgentList model.Cursor = 0 return model, []Intent{{Kind: IntentLoadAgents}} case "Server": model.Screen = ScreenServer model.Cursor = 0 model.StatusMsg = "" return model, []Intent{{Kind: IntentLoadAgents}} case "Quit": return model, []Intent{{Kind: IntentQuit}} } } return model, nil } func updateAgentList(model Model, key KeyMsg) (Model, []Intent) { switch key.Str { case "0": model.Screen = ScreenMain model.Cursor = 0 model.StatusMsg = "" case "up", "k": model.Cursor = clamp(model.Cursor-1, 0, max(0, len(model.Agents)-1)) case "down", "j": model.Cursor = clamp(model.Cursor+1, 0, max(0, len(model.Agents)-1)) case "enter": if model.Cursor < len(model.Agents) { sel := model.Agents[model.Cursor] model.Selected = &sel model.Screen = ScreenAgentActions model.Cursor = 0 model.StatusMsg = "" } } return model, nil } func updateAgentActions(model Model, key KeyMsg) (Model, []Intent) { if model.Selected == nil { model.Screen = ScreenAgentList return model, nil } opts := AgentActionOptions(model.Selected.Enabled) switch key.Str { case "0": model.Screen = ScreenAgentList model.Cursor = 0 model.Selected = nil model.StatusMsg = "" return model, []Intent{{Kind: IntentLoadAgents}} case "up", "k": model.Cursor = clamp(model.Cursor-1, 0, len(opts)-1) case "down", "j": model.Cursor = clamp(model.Cursor+1, 0, len(opts)-1) case "enter": if model.Cursor < len(opts) { return executeAction(model, opts[model.Cursor].Label) } } return model, nil } func executeAction(model Model, action string) (Model, []Intent) { id := model.Selected.ID switch action { 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 model.LogScroll = 0 model.Cursor = 0 return model, []Intent{{Kind: IntentLoadLogs, AgentID: id}} } return model, nil } func updateLogs(model Model, key KeyMsg) (Model, []Intent) { switch key.Str { case "0": if model.Selected != nil { model.Screen = ScreenAgentActions } else { model.Screen = ScreenServer } model.Cursor = 0 model.LogLines = nil model.LogScroll = 0 case "up", "k": model.LogScroll = max(0, model.LogScroll-1) case "down", "j": maxScroll := max(0, len(model.LogLines)-visibleLogLines(model)) model.LogScroll = min(model.LogScroll+1, maxScroll) case "r": return model, []Intent{{Kind: IntentLoadLogs}} } return model, nil } func updateServerScreen(model Model, key KeyMsg) (Model, []Intent) { opts := ServerMenuOptions(model.LauncherRunning) switch key.Str { case "0": model.Screen = ScreenMain model.Cursor = 0 model.StatusMsg = "" case "up", "k": model.Cursor = clamp(model.Cursor-1, 0, len(opts)-1) case "down", "j": model.Cursor = clamp(model.Cursor+1, 0, len(opts)-1) case "enter": if model.Cursor < len(opts) { return executeServerAction(model, opts[model.Cursor].Label) } } return model, nil } func executeServerAction(model Model, action string) (Model, []Intent) { switch action { 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 } // ── pure helpers ───────────────────────────────────────────────────────── func visibleLogLines(m Model) int { lines := m.WindowHeight - 6 if lines < 5 { return 5 } return lines } func clamp(v, lo, hi int) int { if v < lo { return lo } if v > hi { return hi } return v }