diff --git a/agents/runtime.go b/agents/runtime.go index 78c8cc4..4bb73cd 100644 --- a/agents/runtime.go +++ b/agents/runtime.go @@ -259,6 +259,12 @@ func (a *Agent) runLLM(ctx context.Context, msgCtx decision.MessageContext) (str "call_id", tc.ID, ) + // Notify the room that a tool is being called + toolNotice := fmt.Sprintf("🔨 %s", tc.Name) + if err := a.matrix.SendMarkdown(ctx, msgCtx.RoomID, toolNotice); err != nil { + a.logger.Warn("failed to send tool call notice", "tool", tc.Name, "err", err) + } + result := a.toolReg.Execute(ctx, tc.Name, tc.Arguments) output := result.Output diff --git a/pkg/tui/messages.go b/pkg/tui/messages.go index 0156cd5..8b94bfa 100644 --- a/pkg/tui/messages.go +++ b/pkg/tui/messages.go @@ -24,5 +24,14 @@ type MsgServerActionDone struct { Errors []string } +// 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 +} + // MsgTick triggers a periodic refresh. type MsgTick struct{} diff --git a/pkg/tui/model.go b/pkg/tui/model.go index 011c971..3023e1b 100644 --- a/pkg/tui/model.go +++ b/pkg/tui/model.go @@ -64,6 +64,7 @@ func ServerMenuOptions() []MenuOption { {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"}, } } diff --git a/pkg/tui/update.go b/pkg/tui/update.go index 39d60e3..7a52bd2 100644 --- a/pkg/tui/update.go +++ b/pkg/tui/update.go @@ -16,10 +16,11 @@ const ( IntentQuit IntentKind = "quit" // Server-wide bulk operations - IntentStartAll IntentKind = "start_all" - IntentStopAll IntentKind = "stop_all" - IntentRestartAll IntentKind = "restart_all" - IntentKillAll IntentKind = "kill_all" + IntentStartAll IntentKind = "start_all" + IntentStopAll IntentKind = "stop_all" + IntentRestartAll IntentKind = "restart_all" + IntentKillAll IntentKind = "kill_all" + IntentRebuildRestart IntentKind = "rebuild_restart" ) // Intent is pure data describing a side effect to execute. @@ -67,6 +68,16 @@ func Update(model Model, msg interface{}) (Model, []Intent) { } 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 { + 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) @@ -267,6 +278,9 @@ func executeServerAction(model Model, action string) (Model, []Intent) { case "Kill All": model.StatusMsg = "Killing all agents..." return model, []Intent{{Kind: IntentKillAll}} + case "Rebuild & Restart": + model.StatusMsg = "Building & restarting..." + return model, []Intent{{Kind: IntentRebuildRestart}} } return model, nil } diff --git a/shell/process/manager.go b/shell/process/manager.go index 2a7d9ab..29720b6 100644 --- a/shell/process/manager.go +++ b/shell/process/manager.go @@ -379,6 +379,15 @@ func (m *Manager) PidPath(id string) string { return m.pidPath(id) } // LogPath returns the path to the log file for an agent. func (m *Manager) LogPath(id string) string { return m.logPath(id) } +// Build compiles all project binaries by running build.sh. +// Returns the combined output and any error. +func (m *Manager) Build() (string, error) { + cmd := exec.Command("bash", "build.sh") + cmd.Env = m.buildEnv() + out, err := cmd.CombinedOutput() + return string(out), err +} + // ── internal helpers ───────────────────────────────────────────────────── func (m *Manager) pidPath(id string) string { return filepath.Join(m.runDir, id+".pid") } diff --git a/shell/tui/adapter.go b/shell/tui/adapter.go index 358ca8a..2e9cb27 100644 --- a/shell/tui/adapter.go +++ b/shell/tui/adapter.go @@ -4,6 +4,7 @@ package tui import ( "fmt" + "strings" "time" tea "github.com/charmbracelet/bubbletea" @@ -56,6 +57,9 @@ func (a *Adapter) RunIntent(intent puretui.Intent) tea.Cmd { case puretui.IntentKillAll: return a.killAll() + case puretui.IntentRebuildRestart: + return a.rebuildRestart() + case puretui.IntentTick: return a.tick() @@ -267,6 +271,86 @@ func (a *Adapter) killAll() tea.Cmd { } } +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) + } + } + + // 2. Stop all running agents + for _, id := range wasRunning { + _ = a.mgr.Stop(id) + } + if len(wasRunning) > 0 { + time.Sleep(500 * time.Millisecond) + } + + // 3. 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 + } + } + } + // 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} + } + + // 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 + } + 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, + } + } +} + func (a *Adapter) loadLogs(id string) tea.Cmd { return func() tea.Msg { lines, err := a.mgr.LogTail(id, 100)