okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
78011da
main
1package tui 2 3import ( 4"albedo/cli/internal/daemon" 5"fmt" 6"maps" 7"slices" 8"strings" 9 10"github.com/charmbracelet/x/ansi" 11) 12 13func (m AgentsViewModel )paneHeader () []string { 14// One column goes to the space after the divider. 15width := m .paneWidth ()- 1 16if width <= 0 { 17width = max (1 ,m .Width - 2 ) 18 } 19rows := make ([]string ,0 ,12 ) 20n := m .nodes [m .selected ] 21if n == nil || width <= 0 { 22return rows 23 } 24colors := agentColors () 25styled := func (hue rgb ,s string )string {return sgr (hue .hex (),"" )+ s + ansiReset } 26state := "idle" 27switch { 28case n .id == agentsYou : 29state = "that's you" 30case n .closed : 31state = "closed" 32case n .session .Status .Phase != nil : 33switch * n .session .Status .Phase { 34case daemon .PhasePreparing : 35state = "preparing" 36case daemon .PhaseInterrupted : 37state = "interrupting" 38case daemon .PhaseIdle ,daemon .PhaseResting : 39state = "idle" 40default : 41state = "running" 42 } 43case n .running : 44state = "running" 45case n .peer : 46state = "outside this tree" 47 } 48rows = append (rows ,styled (n .hue ,"● " )+ DefaultStyles .Bold .Render (n .name )+ " " + DefaultStyles .Faint .Render (state )) 49var meta []string 50if n .model != "" { 51meta = append (meta ,n .model ) 52 } 53if n .id != agentsYou { 54meta = append (meta ,fmt .Sprintf ("depth %d" ,n .depth )) 55 } 56if parent := m .nodes [n .parent ];parent != nil { 57meta = append (meta ,"parent " + parent .name ) 58 } 59rows = append (rows ,DefaultStyles .Faint .Render (ansi .Truncate (strings .Join (meta ," · " ),width ,"…" )),"" ) 60for _ ,field := range []struct {label ,text string }{{"request" ,n .currentRequest }, {"latest" ,n .latestProgress }} { 61if field .text == "" { 62continue 63 } 64rows = append (rows ,DefaultStyles .Muted .Render (field .label )) 65wrapped := strings .Split (ansi .Wrap (field .text ,max (1 ,width )," " ),"\n" ) 66if len (wrapped )> 3 { 67wrapped = wrapped [:3 ] 68wrapped [2 ]= ansi .Truncate (wrapped [2 ],max (1 ,width - 1 ),"" )+ "…" 69 } 70rows = append (rows ,wrapped ... ) 71 } 72if len (n .> 0 { 73rows = append (rows ,DefaultStyles .Muted .Render ("mail" )) 74for _ ,:= range n .max (0 ,len (n .- 5 ):] { 75var arrow string 76if incoming { 77arrow = styled (colors ."← " ) 78 }else { 79arrow = DefaultStyles .Faint .Render ("→ " ) 80 } 81rows = append (rows ,ansi .Truncate (arrow + who + " " + DefaultStyles .Faint .Render (kind ),width ,"…" )) 82 } 83rows = append (rows ,"" ) 84 } 85live := DefaultStyles .Muted .Render ("tail" ) 86if n .running { 87live += " " + styled (n .hue ,"●" )+ DefaultStyles .Faint .Render (" live" ) 88 } 89if n .tail .omitted || n .preview .omitted { 90live += DefaultStyles .Faint .Render (" · earlier output omitted" ) 91 } 92return append (rows ,live ) 93} 94 95func (m AgentsViewModel )pane (height int ) []string { 96rows := m .paneHeader () 97room := max (0 ,height - len (rows )) 98if room == 0 || len (rows )== 0 { 99return rows 100 } 101wrapped := m .tailCache .rows 102if len (wrapped )== 0 { 103return append (rows ,DefaultStyles .Faint .Render ("nothing yet" )) 104 } 105return append (rows ,wrapped [max (0 ,len (wrapped )- room ):]... ) 106} 107 108// drawTail wraps one tail line to width and styles it by kind. 109func drawTail (line tailLine ,width ,limit int ) []string { 110gutter ,inner := "" ,width 111format := func (s ... string )string {return strings .Join (s ,"" ) } 112switch line .kind { 113case tailThinking : 114format = func (s ... string )string {return DefaultStyles .Faint .Render (ansiItalic + s [0 ]) } 115case tailCode : 116gutter ,inner ,format = DefaultStyles .Decor .Render ("│ " ),width - 2 ,DefaultStyles .Muted .Render 117case tailOutput : 118gutter ,inner ,format = DefaultStyles .Decor .Render ("⎿ " ),width - 2 ,DefaultStyles .Faint .Render 119case tailMeta : 120format = DefaultStyles .Faint .Render 121 } 122wrapped := ansi .Hardwrap (line .text ,max (8 ,inner ),true ) 123start := len (wrapped ) 124for range limit { 125index := strings .LastIndexByte (wrapped [:start ],'\n' ) 126if index < 0 { 127start = 0 128break 129 } 130start = index 131 } 132if start > 0 { 133start ++ 134 } 135var out []string 136for part := range strings .SplitSeq (wrapped [start :],"\n" ) { 137// A cached row must not retain the rest of a long wrapped line. 138out = append (out ,gutter + format (strings .Clone (part ))) 139 } 140return out 141} 142 143func (m AgentsViewModel )View ()string { 144if m .Width <= 0 || m .Height <= 0 { 145return "" 146 } 147live ,tokens := 0 ,0 148for id ,n := range m .nodes { 149if id == agentsYou { 150continue 151 } 152if n .running { 153live ++ 154 } 155tokens += n .chars / 4 156 } 157right := DefaultStyles .Faint .Render (fmt .Sprintf ("%d agents · %d running · ▴%s tok" ,len (m .nodes )- 1 ,live ,compactCount (tokens ))) 158out := []string {titleRule (m .Width ,brand ("albedo" )+ " " + DefaultStyles .Muted .Render ("/agents" ),right )} 159 160var confirmRows []string 161if m .confirm .asking () { 162confirmRows = strings .Split (m .confirm .footer (max (1 ,m .Width ),"" ),"\n" ) 163 } 164bodyH := max (1 ,m .Height - 3 - max (1 ,len (confirmRows ))) 165dagW := m .dagWidth () 166// Scroll so the selected dot stays in view on a tall swarm. 167offset := 0 168if n := m .nodes [m .selected ];n != nil && n .y + 3 > bodyH { 169offset = n .y + 3 - bodyH 170 } 171pw := m .paneWidth () 172stacked := 0 173if pw == 0 { 174stacked = min (len (m .paneHeader ()),max (1 ,bodyH / 2 )) 175bodyH = max (1 ,bodyH - stacked ) 176offset = 0 177if n := m .nodes [m .selected ];n != nil && n .y + 3 > bodyH { 178offset = n .y + 3 - bodyH 179 } 180 } 181canvas := newCanvas (dagW ,bodyH + offset ) 182m .drawEdges (canvas ) 183m .drawNodes (canvas ) 184pane := m .pane (bodyH ) 185divider := DefaultStyles .Decor .Render ("│" ) 186for y := range bodyH { 187row := canvas .line (y + offset ) 188if pw > 0 { 189side := "" 190if y < len (pane ) { 191side = pane [y ] 192 } 193row += divider + " " + ansi .Truncate (side ,pw - 1 ,"…" ) 194 } 195out = append (out ,row ) 196 } 197 198if stacked > 0 { 199rows := m .paneHeader () 200for i := range stacked { 201row := "" 202if i < len (rows ) { 203row = rows [i ] 204 } 205out = append (out ,ansi .Truncate (row ,max (1 ,m .Width ),"…" )) 206 } 207 } 208 209status := "" 210switch { 211case m .rename .active (): 212status = DefaultStyles .Muted .Render ("rename " )+ DefaultStyles .Bold .Render (m .label (m .rename .id ,"" )) 213if n := m .nodes [m .rename .id ];n != nil && n .address != "" { 214status += DefaultStyles .Faint .Render (" · its family can still mail it at " )+ DefaultStyles .Muted .Render (n .address ) 215 } 216case m .err != nil : 217status = DefaultStyles .Error .Render (m .err .Error ()+ " · ctrl+l retries" ) 218case len (m .pendingOperations )> 0 : 219ids := slices .Sorted (maps .Keys (m .pendingOperations )) 220status = DefaultStyles .Error .Render ("Unresolved request: " + strings .Join (ids ,", " )) 221case m .noticeT > 0 : 222status = DefaultStyles .Muted .Render (m .notice ) 223 } 224if len (confirmRows )> 0 { 225out = append (out ,confirmRows ... ) 226 }else { 227out = append (out ,status ) 228 } 229target := m .label (m .selected ,"agent" ) 230input := m .input .View () 231if m .input .Value ()== "" { 232input = DefaultStyles .Faint .Render ("message " + target + "… or /spawn <name> <task>" ) 233 } 234if n := m .nodes [m .rename .id ];n != nil && m .rename .active () { 235out = append (out ,DefaultStyles .Prompt .Render ("✎ " )+ m .rename .view (m .Width - promptMarkWidth ),renameHints ("restores " + renameBlank (n ))) 236 }else { 237out = append (out ,promptLead ()+ input ,keyHints (hint {"tab" ,"next agent" },hint {"enter" ,"open or send" },hint {"ctrl+r" ,"rename" },hint {"ctrl+x" ,"delete" },hint {"esc" ,"back" })) 238 } 239return strings .Join (out ,"\n" ) 240}