129 lines
3.0 KiB
Go
129 lines
3.0 KiB
Go
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"))
|
|
}
|