package tui import ( "context" "fmt" "strings" "sync" tea "github.com/charmbracelet/bubbletea" "agentu/internal/agent" ) // toolApprovalMsg is sent to the TUI when a mutating tool call needs approval. type toolApprovalMsg struct { request agent.ToolAuthorizationRequest respond chan agent.ToolAuthorizationDecision } // ApprovalBroker bridges the agent's ToolAuthorizer interface with the TUI's // event loop, allowing interactive approval of mutating tool calls. type ApprovalBroker struct { mu sync.Mutex events chan<- tea.Msg } // NewApprovalBroker creates a new approval broker. func NewApprovalBroker() *ApprovalBroker { return &ApprovalBroker{} } // SetEvents sets the current event channel for sending approval requests. func (b *ApprovalBroker) SetEvents(events chan<- tea.Msg) { b.mu.Lock() defer b.mu.Unlock() b.events = events } // ClearEvents removes the current event channel. func (b *ApprovalBroker) ClearEvents() { b.mu.Lock() defer b.mu.Unlock() b.events = nil } // AuthorizeToolCall implements agent.ToolAuthorizer by sending an approval // request to the TUI and waiting for the user's decision. func (b *ApprovalBroker) AuthorizeToolCall(ctx context.Context, req agent.ToolAuthorizationRequest) (agent.ToolAuthorizationDecision, error) { b.mu.Lock() events := b.events b.mu.Unlock() if events == nil { return "", fmt.Errorf("approval UI is not available") } respond := make(chan agent.ToolAuthorizationDecision, 1) msg := toolApprovalMsg{ request: req, respond: respond, } select { case events <- msg: case <-ctx.Done(): return "", ctx.Err() } select { case decision := <-respond: return decision, nil case <-ctx.Done(): return "", ctx.Err() } } func (m *model) updateApproval(msg tea.KeyMsg) (tea.Model, tea.Cmd) { if m.approval == nil { return *m, nil } switch msg.String() { case "y", "Y": m.respondToApproval(agent.ToolAuthorizationAllow) return *m, nil case "n", "N", "esc": m.respondToApproval(agent.ToolAuthorizationDeny) return *m, nil case "ctrl+c": m.respondToApproval(agent.ToolAuthorizationDeny) if m.cancel != nil { m.cancel() } return *m, tea.Quit default: return *m, nil } } func (m *model) respondToApproval(decision agent.ToolAuthorizationDecision) { request := m.approval m.approval = nil if request != nil { request.respond <- decision } m.status = "working..." m.resize() m.refreshViewport(true) } func (m model) approvalView() string { if m.approval == nil { return "" } req := m.approval.request preview := strings.TrimSpace(req.Preview) if preview == "" { preview = strings.TrimSpace(req.Arguments) } lines := []string{ m.styles.ErrorLabel.Render("Approve " + req.ToolName + "?"), m.styles.ToolValue.Render(preview), } if req.RollbackAvailable { lines = append(lines, m.styles.Muted.Render("A rollback snapshot will be created.")) } lines = append(lines, m.styles.Muted.Render("y approve ยท n/esc deny")) return m.styles.InputBox.Width(max(20, m.width)).Render(strings.Join(lines, "\n")) }