feat: complete v0.3.1 agent workflow
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
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"))
|
||||
}
|
||||
Reference in New Issue
Block a user