cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / ui / agentask.go
agentask.go 4.7K
1package ui23import (4 "strconv"56 "github.com/charmbracelet/x/ansi"78 tea "charm.land/bubbletea/v2"910 "src.rest/cat/012/internal/cowork"11)1213// The agent's questions (the ask tool) and requests (to run notebook14// cells, to enter JEV formulas directly) wait on the room's board until15// the person it asks is free: in READY, nothing open. Then the question16// takes the context line, a field at a time: a yes or no, a choice, or17// a value typed as a prompt. Esc cancels; the answer goes back to the18// agent as an elicitation's result.1920// agentAsks opens the next question waiting for this session, when the21// person is free, and lets go of one the agent withdrew.22func (m *Model) agentAsks() tea.Cmd {23 seat := m.share.seat24 if seat == nil {25 return nil26 }27 if q := m.agents.asking; q != nil {28 if q.Done() {29 m.agents.asking = nil30 m.closeAsk()31 m.note = q.Agent + " stopped waiting for an answer"32 }33 return nil34 }35 if m.mode != modeReady || m.overlay != nil {36 return nil37 }38 b := m.board()39 q := b.NextAsk(seat.ID(), func(id int) bool {40 _, here := seat.Peer(id)41 return here || id == seat.ID()42 })43 if q == nil {44 return nil45 }46 m.agents.asking = q47 if q.Kind == cowork.Grant {48 m.askGrant(q)49 } else {50 m.askField(q, 0, map[string]any{})51 }52 return nil53}5455// closeAsk closes the question on the context line.56func (m *Model) closeAsk() {57 switch {58 case m.mode == modePrompt:59 m.closePrompt()60 case m.overlay != nil:61 m.closeOverlay()62 }63}6465// answerAsk sends the person's answer to the agent.66func (m *Model) answerAsk(q *cowork.Ask, ans cowork.Answer) {67 m.agents.asking = nil68 if m.share.seat != nil {69 m.board().Answer(q, ans)70 }71 switch ans.Action {72 case "accept":73 m.note = "Answered " + q.Agent74 case "decline":75 m.note = "Told " + q.Agent + " no"76 default:77 m.note = "Didn't answer " + q.Agent78 }79}8081// askHead is how a question starts on the context line, cut to leave82// its choices room.83func (m *Model) askHead(q *cowork.Ask, text string) string {84 return ansi.Truncate("◆ "+q.Agent+" asks: "+text, max(m.width-44, 24), "…")85}8687// askField asks field i of q, the answers so far in content; past the88// last, the answer goes back.89func (m *Model) askField(q *cowork.Ask, i int, content map[string]any) {90 cancel := func(m *Model) tea.Cmd { m.answerAsk(q, cowork.Answer{Action: "cancel"}); return nil }91 if len(q.Fields) == 0 {92 m.ask(question{msg: m.askHead(q, q.Message), desc: q.Message, choices: []choice{93 {key: "enter", label: "Yes", run: func(m *Model) tea.Cmd { m.answerAsk(q, cowork.Answer{Action: "accept"}); return nil }},94 {key: "n", label: "No", run: func(m *Model) tea.Cmd { m.answerAsk(q, cowork.Answer{Action: "decline"}); return nil }},95 {key: "esc", label: "Cancel", run: cancel},96 }})97 return98 }99 if i == len(q.Fields) {100 m.answerAsk(q, cowork.Answer{Action: "accept", Content: content})101 return102 }103 f := q.Fields[i]104 head := m.askHead(q, q.Message+" "+f.Label())105 if i > 0 {106 head = m.askHead(q, f.Label())107 }108 next := func(v any) func(m *Model) tea.Cmd {109 return func(m *Model) tea.Cmd {110 content[f.Name] = v111 m.askField(q, i+1, content)112 return nil113 }114 }115 switch f.Kind {116 case cowork.Boolean:117 m.ask(question{msg: head + "?", desc: f.Description, choices: []choice{118 {key: "y", label: "Yes", run: next(true)}, {key: "n", label: "No", run: next(false)}, {key: "esc", label: "Cancel", run: cancel}}})119 case cowork.Choice:120 var chs []choice121 for j := range f.Choices {122 chs = append(chs, choice{key: strconv.Itoa(j + 1), label: f.ChoiceLabel(j), run: next(f.Choices[j])})123 }124 m.ask(question{msg: head, desc: f.Description, choices: append(chs, choice{key: "esc", label: "Cancel", run: cancel})})125 default:126 m.askText(head+":", "", func(text string) {127 v, err := f.Parse(text)128 if err != nil {129 m.fail(err.Error())130 m.askField(q, i, content)131 return132 }133 next(v)(m)134 }, func() { cancel(m) })135 }136}137138// askGrant asks leave for what the agent wants to do beyond changing139// cells, once or for the session.140func (m *Model) askGrant(q *cowork.Ask) {141 answer := func(ans cowork.Answer) func(m *Model) tea.Cmd {142 return func(m *Model) tea.Cmd { m.answerAsk(q, ans); return nil }143 }144 desc := "Notebook cells run with nu as you, with your files, as an untrusted macro would"145 if q.Grant == "jev" {146 desc = "JEV formulas send cells' values to TypeSafe's model with your API key"147 }148 m.ask(question{msg: ansi.Truncate("◆ "+q.Message+".", max(m.width-64, 20), "…"), warn: true, desc: desc, choices: []choice{149 {key: "enter", label: "Allow once", run: answer(cowork.Answer{Action: "accept"})},150 {key: "a", label: "Allow for this session", run: answer(cowork.Answer{Action: "accept", Always: true})},151 {key: "esc", label: "Deny", run: answer(cowork.Answer{Action: "decline"})},152 }})153}