okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
78011da
main
1package tui 2 3import ( 4"cmp" 5"context" 6"errors" 7"fmt" 8"slices" 9"strings" 10"time" 11 12"albedo/cli/internal/daemon" 13 14 tea"charm.land/bubbletea/v2" 15) 16 17// below counts the agents under id. 18func (m AgentsViewModel )below (id string )int { 19count := 0 20for _ ,n := range m .nodes { 21for p := n .parent ;p != "" ; { 22if p == id { 23count ++ 24break 25 } 26if m .nodes [p ]== nil { 27break 28 } 29p = m .nodes [p ].parent 30 } 31 } 32return count 33} 34 35func (m AgentsViewModel )deleteCmd (id string ) tea.Cmd { 36conn ,gen ,name := m .Conn ,m .viewGen ,m .label (id ,"" ) 37condition := daemon.SessionCondition {} 38if n := m .nodes [id ];n != nil { 39condition = daemon.SessionCondition {ETag :n .session .ETag ,FamilyRevision :n .session .FamilyRevision } 40 } 41return func () tea.Msg { 42res ,err := daemon .DeleteSession (context .Background (),conn ,id ,true ,condition ) 43if err != nil { 44return agentsSentMsg {Gen :gen ,Action :"delete " + name ,Err :err } 45 } 46return agentsSentMsg {Gen :gen ,Notice :name + ": " + res .Message } 47 } 48} 49 50func (m * AgentsViewModel )sendCmd (id ,text string ) tea.Cmd { 51conn ,gen ,target := m .Conn ,m .viewGen ,m .label (id ,"agent" ) 52handle ,err := daemon .NewSubmission (id , daemon.SubmissionRequest {Content :& text }) 53if err == nil { 54m .pendingOperations [handle .ID ()]= agentPendingOperation {Handle :handle ,Action :"send a message to " + target ,Draft :text ,InFlight :true } 55 } 56return func () tea.Msg { 57result := agentsSentMsg {Gen :gen ,Action :"send a message to " + target ,Handle :handle ,Draft :text ,Err :err } 58if err == nil { 59_ ,result .Err = daemon .SubmitOperation (context .Background (),conn ,handle ) 60 } 61return result 62 } 63} 64 65func (m * AgentsViewModel )spawnCmd (parent ,name ,task string ) tea.Cmd { 66conn ,gen := m .Conn ,m .viewGen 67draft := "/spawn " + name + " " + task 68if name == "" || task == "" { 69return func () tea.Msg { 70return agentsSentMsg {Gen :gen ,Action :"start an agent" ,Draft :draft ,Err :errors .New ("use /spawn <name> <task>" )} 71 } 72 } 73handle ,err := daemon .NewCreation (daemon.CreateSessionRequest {Kind :"child" ,ParentID :parent ,Address :name ,Name :name ,Task :task }) 74if err == nil { 75m .pendingOperations [handle .ID ()]= agentPendingOperation {Handle :handle ,Action :"start " + name ,Draft :draft ,InFlight :true } 76 } 77return func () tea.Msg { 78result := agentsSentMsg {Gen :gen ,Action :"start " + name ,Notice :"spawned " + name ,Handle :handle ,Draft :draft ,Err :err } 79if err == nil { 80_ ,result .Err = daemon .CreateSessionOperation (context .Background (),conn ,handle ) 81 } 82return result 83 } 84} 85 86func agentOperationTick (id string ,gen int ) tea.Cmd { 87return tea .Tick (300 * time .Millisecond ,func (time.Time ) tea.Msg {return agentsOperationPollMsg {ID :id ,Gen :gen } }) 88} 89 90func (m AgentsViewModel )resolveAgentOperation (pending agentPendingOperation ) tea.Cmd { 91conn ,gen := m .Conn ,m .viewGen 92return func () tea.Msg { 93result := agentsSentMsg {Gen :gen ,Action :pending .Action ,Handle :pending .Handle ,Draft :pending .Draft ,Recovery :true ,Notice :"Confirmed: " + pending .Action } 94if pending .Handle .IsCreation () { 95_ ,result .Err = daemon .ResolveCreation (context .Background (),conn ,pending .Handle ) 96 }else { 97receipt ,err := daemon .ResolveOperation (context .Background (),conn ,pending .Handle ) 98result .Err = err 99if err == nil { 100result .Err = receipt .Rejection () 101if receipt .Delivery != nil && * receipt .Delivery == "cancelled" { 102result .Notice = "The pending input was cancelled." 103 } 104 } 105 } 106return result 107 } 108} 109 110// askDelete asks before deleting an agent, and says what goes with it. 111func (m * AgentsViewModel )askDelete (id string ) { 112what := m .label (id ,"" ) 113switch below := m .below (id ); { 114case below == 1 : 115what += " and the agent below it" 116case below > 1 : 117what += fmt .Sprintf (" and the %d agents below it" ,below ) 118 } 119m .confirm .ask ("delete" ,id ,"delete" ,"Their transcripts and work will also be deleted. Delete " + what + "?" ) 120} 121 122func (m AgentsViewModel )key (msg tea.KeyPressMsg ) (AgentsViewModel , tea.Cmd ) { 123s ,empty := msg .String (),m .input .Value ()== "" 124if m .rename .active () { 125return m ,m .rename .key (msg ) 126 } 127if m .confirm .asking () { 128id := m .confirm .target 129if m .confirm .key (msg ) { 130m .confirm .dismiss () 131m .say ("deleting " + m .label (id ,"" )+ "…" ) 132return m ,m .deleteCmd (id ) 133 } 134if ! m .confirm .asking () { 135m .say ("kept" ) 136 } 137return m ,nil 138 } 139switch { 140case s == "ctrl+l" : 141return m ,m .restartStream () 142case s == "ctrl+r" : 143if n := m .nodes [m .selected ];n != nil && n .id != agentsYou { 144m .rename .open (n .id ,n .name ,"name this agent" ) 145if n := m .nodes [m .rename .id ];n != nil { 146m .rename .etag = n .session .ETag 147 } 148 } 149return m ,nil 150case s == "ctrl+x" : 151switch n := m .nodes [m .selected ]; { 152case n == nil || n .id == agentsYou : 153case n .id == m .SessionID : 154m .say ("this is the session you opened the view from; delete it from the session browser" ) 155default : 156m .askDelete (n .id ) 157 } 158return m ,nil 159case s == "esc" || s == "ctrl+c" || s == "ctrl+o" : 160if ! empty && s == "esc" { 161m .input .SetValue ("" ) 162return m ,nil 163 } 164return m ,func () tea.Msg {return AgentsDoneMsg {} } 165case s == "tab" || (empty && (s == "right" || s == "down" )): 166m .cycle (1 ) 167return m ,m .seedCmd () 168case s == "shift+tab" || (empty && (s == "left" || s == "up" )): 169m .cycle (- 1 ) 170return m ,m .seedCmd () 171case s == "enter" : 172n := m .nodes [m .selected ] 173if n == nil { 174return m ,nil 175 } 176text := strings .TrimSpace (m .input .Value ()) 177if text == "" { 178if n .session .ID == "" { 179n .session = daemon.Session {ID :n .id ,Title :n .name ,Model :n .model } 180 } 181sess := n .session 182return m ,func () tea.Msg {return AgentsAttachMsg {Session :sess } } 183 } 184m .input .SetValue ("" ) 185if rest ,ok := strings .CutPrefix (text ,"/spawn " );ok { 186name ,task ,_ := strings .Cut (strings .TrimSpace (rest )," " ) 187return m ,m .spawnCmd (n .id ,name ,strings .TrimSpace (task )) 188 } 189return m ,m .sendCmd (n .id ,text ) 190 } 191var cmd tea.Cmd 192m .input ,cmd = m .input .Update (msg ) 193return m ,cmd 194} 195 196// renameBlank is what an agent is called once its given name is cleared. 197func renameBlank (n * agentNode )string { 198return cmp .Or (n .address ,"its latest message's title" ) 199} 200 201func (m * AgentsViewModel )say (text string ) { 202m .notice = text 203m .noticeT = 3 204} 205 206func (m * AgentsViewModel )cycle (d int ) { 207if n := len (m .order );n > 0 { 208i := max (0 ,slices .Index (m .order ,m .selected )) 209m .selected = m .order [(i + d + n )% n ] 210 } 211}