okami.mom/albedo
git clone https://git.nkp.pet/okami.mom/albedo
6e2130b
main
1package tui 2 3import ( 4"fmt" 5"math" 6"slices" 7"strings" 8"time" 9 10"github.com/charmbracelet/x/ansi" 11) 12 13type agentPacket struct { 14to string 15edge string 16path [][2 ]int 17hue rgb 18pos float64 19blocks int 20tokens int 21} 22 23type agentFloat struct { 24text string 25hue rgb 26x ,y int 27t float64 28} 29 30func (m * AgentsViewModel )dagWidth ()int { 31w := m .paneWidth () 32if w > 0 { 33return m .Width - w - 1 34 } 35return m .Width 36} 37 38func (m * AgentsViewModel )paneWidth ()int { 39if m .Width < 96 { 40return 0 41 } 42return min (46 ,max (34 ,m .Width / 3 )) 43} 44 45// layout places dots by depth, each depth wrapped into rows that fit the 46// pane, parents before their children so a family stays together. 47func (m * AgentsViewModel )layout () { 48// The app sizes every screen, including this one before it first opens. 49if m .nodes == nil { 50return 51 } 52you := m .node (agentsYou ,agentsYou ) 53you .x ,you .y = 2 ,2 54children := map [string ][]string {} 55var roots ,peers []string 56for id ,n := range m .nodes { 57switch { 58case id == agentsYou : 59case n .peer : 60peers = append (peers ,id ) 61case n .parent == "" || m .nodes [n .parent ]== nil : 62roots = append (roots ,id ) 63default : 64children [n .parent ]= append (children [n .parent ],id ) 65 } 66 } 67for _ ,list := range children { 68slices .Sort (list ) 69 } 70slices .Sort (roots ) 71slices .Sort (peers ) 72levels := [][]string {append (roots ,peers ... )} 73for { 74var next []string 75for _ ,id := range levels [len (levels )- 1 ] { 76next = append (next ,children [id ]... ) 77 } 78if len (next )== 0 { 79break 80 } 81levels = append (levels ,next ) 82 } 83width := max (agentsSlotW ,m .dagWidth ()- 4 ) 84perRow := max (1 ,width / agentsSlotW ) 85y := 5 86m .order = m .order [:0 ] 87for _ ,level := range levels { 88for start := 0 ;start < len (level );start += perRow { 89row := level [start :min (len (level ),start + perRow )] 90used := len (row )* agentsSlotW 91x := 2 + max (0 , (width - used )/ 2 ) 92for i ,id := range row { 93n := m .nodes [id ] 94n .x ,n .y = x + i * agentsSlotW ,y 95m .order = append (m .order ,id ) 96 } 97y += 3 98 } 99y += agentsLevelGp 100 } 101if m .nodes [m .selected ]== nil && len (m .order )> 0 { 102m .selected = m .order [0 ] 103 } 104} 105 106// route runs from a to b: down from a, across the row above b, into b. A 107// route upward is the same route reversed. 108func route (a ,b * agentNode ) [][2 ]int { 109if b .y < a .y { 110cells := route (b ,a ) 111slices .Reverse (cells ) 112return cells 113 } 114var cells [][2 ]int 115channel := b .y - 1 116if b .y == a .y { 117channel = a .y - 1 118cells = append (cells , [2 ]int {a .x ,a .y - 1 }) 119 } 120for r := a .y + 1 ;r <= channel ;r ++ { 121cells = append (cells , [2 ]int {a .x ,r }) 122 } 123step := 1 124if b .x < a .x { 125step = - 1 126 } 127for c := a .x + step ;c != b .x + step ;c += step { 128cells = append (cells , [2 ]int {c ,channel }) 129 } 130return cells 131} 132 133func edgeKey (a ,b string )string {return a + ">" + b } 134 135func (m * AgentsViewModel )send (from ,to string ,tokens int ) { 136a ,b := m .nodes [from ],m .nodes [to ] 137if a == nil || b == nil || from == to { 138return 139 } 140path := route (a ,b ) 141if len (path )== 0 { 142return 143 } 144blocks := max (1 ,min (5 ,int (math .Round (math .Log2 (float64 (tokens )/ 200 + 1 ))))) 145var key string 146if a .parent == to { 147key = edgeKey (to ,from ) 148 }else { 149key = edgeKey (from ,to ) 150 } 151m .packets = append (m .packets ,agentPacket {path :path ,pos :- float64 (blocks ),blocks :blocks ,hue :a .hue ,to :to ,tokens :tokens ,edge :key }) 152m .heat [key ]= 1 153} 154 155// step advances one frame of animation. 156func (m * AgentsViewModel )step () { 157now := time .Now () 158dt := math .Min (0.2 ,now .Sub (m .last ).Seconds ()) 159m .last = now 160m .clock += dt 161m .noticeT = math .Max (0 ,m .noticeT - dt ) 162live := m .packets [:0 ] 163for _ ,p := range m .packets { 164p .pos += 30 * dt 165m .heat [p .edge ]= 1 166if p .pos - float64 (p .blocks )>= float64 (len (p .path )- 1 ) { 167if n := m .nodes [p .to ];n != nil { 168n .flash = 1 169m .floats = append (m .floats ,agentFloat {x :n .x + 2 ,y :n .y - 1 ,text :"▴" + compactCount (p .tokens ),hue :p .hue }) 170 } 171continue 172 } 173live = append (live ,p ) 174 } 175m .packets = live 176for key ,h := range m .heat { 177if h -= dt * 0.8 ;h <= 0 { 178delete (m .heat ,key ) 179 }else { 180m .heat [key ]= h 181 } 182 } 183// an edge still carrying a packet stays hot 184for _ ,p := range m .packets { 185m .heat [p .edge ]= 1 186 } 187floats := m .floats [:0 ] 188for _ ,f := range m .floats { 189f .t += dt 190if f .t < 1.4 { 191floats = append (floats ,f ) 192 } 193 } 194m .floats = floats 195for _ ,n := range m .nodes { 196n .flash = math .Max (0 ,n .flash - dt * 2.5 ) 197n .rate *= math .Exp (- dt * 1.5 ) 198 } 199} 200 201// moving is whether the next frame can differ from this one: running agents 202// pulse, and packets, heat, floats, flashes, and notices play out. 203func (m AgentsViewModel )moving ()bool { 204if len (m .packets )> 0 || len (m .heat )> 0 || len (m .floats )> 0 || m .noticeT > 0 { 205return true 206 } 207for _ ,n := range m .nodes { 208if n .running || n .flash > 0 { 209return true 210 } 211 } 212return false 213} 214 215func compactCount (n int )string { 216if n >= 1000 { 217return fmt .Sprintf ("%.1fk" ,float64 (n )/ 1000 ) 218 } 219return fmt .Sprintf ("%d" ,n ) 220} 221 222var agentGlyphs = map [int ]rune {5 :'│' ,10 :'─' ,6 :'╭' ,12 :'╮' ,3 :'╰' ,9 :'╯' ,7 :'├' ,13 :'┤' ,14 :'┬' ,11 :'┴' ,15 :'┼' ,1 :'│' ,4 :'│' ,2 :'─' ,8 :'─' } 223 224func (m * AgentsViewModel )drawEdges (c * agentCanvas ) { 225type mark struct { 226mask int 227heat float64 228hue rgb 229dotted bool 230 } 231marks := map [[2 ]int ]* mark {} 232// dir is one bit per compass direction; a cell's mask names the arms meeting in it. 233dir := func (dx ,dy int )int { 234switch { 235case dy < 0 : 236return 1 237case dy > 0 : 238return 4 239case dx > 0 : 240return 2 241default : 242return 8 243 } 244 } 245draw := func (from ,to * agentNode ,heat float64 ,dotted bool ) { 246cells := append ([][2 ]int {{from .x ,from .y }},route (from ,to )... ) 247for i := 1 ;i < len (cells );i ++ { 248cell ,prev := cells [i ],cells [i - 1 ] 249mk := marks [cell ] 250if mk == nil { 251mk = & mark {hue :from .hue } 252marks [cell ]= mk 253 } 254next := [2 ]int {to .x ,to .y } 255if i + 1 < len (cells ) { 256next = cells [i + 1 ] 257 } 258mk .mask |= dir (prev [0 ]- cell [0 ],prev [1 ]- cell [1 ])| dir (next [0 ]- cell [0 ],next [1 ]- cell [1 ]) 259if heat >= mk .heat { 260mk .heat ,mk .hue = heat ,from .hue 261 } 262mk .dotted = mk .dotted || dotted 263 } 264 } 265crowded := len (m .nodes )> 24 266for _ ,n := range m .nodes { 267parent := m .nodes [n .parent ] 268if parent == nil || n .peer { 269continue 270 } 271heat := m .heat [edgeKey (n .parent ,n .id )] 272if crowded && heat == 0 { 273continue 274 } 275draw (parent ,n ,heat ,false ) 276 } 277if root := m .nodes [m .root ];root != nil { 278draw (m .nodes [agentsYou ],root ,m .heat [edgeKey (agentsYou ,m .root )]+ m .heat [edgeKey (m .root ,agentsYou )],false ) 279 } 280for key ,heat := range m .heat { 281from ,to ,_ := strings .Cut (key ,">" ) 282a ,b := m .nodes [from ],m .nodes [to ] 283if a == nil || b == nil || b .parent == from || (from == agentsYou && to == m .root )|| (to == agentsYou && from == m .root ) { 284continue 285 } 286draw (a ,b ,heat ,true ) 287 } 288decor := agentColors ().decor 289for cell ,mk := range marks { 290g := agentGlyphs [mk .mask ] 291if g == 0 { 292g = '·' 293 } 294if mk .dotted { 295switch { 296case mk .mask & 5 != 0 && mk .mask & 10 == 0 : 297g = '┆' 298case mk .mask & 10 != 0 && mk .mask & 5 == 0 : 299g = '┄' 300 } 301 } 302c .put (cell [0 ],cell [1 ],string (g ),decor .mix (mk .hue ,math .Min (1 ,mk .heat * 1.1 ))) 303 } 304} 305 306func (m * AgentsViewModel )drawNodes (c * agentCanvas ) { 307t ,colors := m .clock ,agentColors () 308for id ,n := range m .nodes { 309glyph := "●" 310var hue rgb 311switch { 312case id == agentsYou : 313glyph ,hue = "◆" ,colors .you 314case n .running : 315p := 0.5 + 0.5 * math .Sin (t * 5 + n .phase ) 316if p > 0.55 { 317glyph = "◉" 318 } 319hue = n .hue .mix (colors .faint ,0.2 ).mix (n .hue ,p ) 320case n .closed : 321glyph ,hue = "✓" ,n .hue .mix (colors .faint ,0.5 ) 322default : 323glyph = "○" 324if n .peer { 325glyph = "◇" 326 } 327hue = n .hue .mix (colors .faint ,0.3 ) 328 } 329if n .flash > 0 { 330hue = hue .mix (colors .hi ,n .flash * 0.7 ) 331 } 332c .put (n .x ,n .y ,glyph ,hue ) 333name := ansi .Truncate (n .name ,agentsSlotW - 4 ,"…" ) 334label := n .hue .mix (colors .hi ,0.35 ) 335if id == m .selected { 336label = colors .hi 337c .put (n .x - 1 ,n .y ,"[" ,n .hue ) 338c .put (n .x + 2 + ansi .StringWidth (name ),n .y ,"]" ,n .hue ) 339 } 340c .put (n .x + 2 ,n .y ,name ,label ) 341if id == agentsYou { 342continue 343 } 344// The jam: five bars riding the token rate. 345var bars strings.Builder 346level := math .Min (1 ,n .rate / 400 ) 347for i := range 5 { 348v := 0.0 349if n .running { 350v = 0.15 + level * (0.5 + 0.35 * math .Sin (t * (6 + float64 (i )* 1.7 )+ n .phase * float64 (i + 1 )))+ 0.15 * math .Sin (t * 2.3 + float64 (i )) 351 } 352bars .WriteRune (agentsBars [max (0 ,min (7 ,int (math .Round (v * 7 ))))]) 353 } 354barHue := colors .decor 355if n .running { 356barHue = n .hue 357 } 358c .put (n .x + 2 ,n .y + 1 ,bars .String (),barHue ) 359if n .chars > 0 { 360c .put (n .x + 8 ,n .y + 1 ,compactCount (n .chars / 4 ),colors .faint ) 361 } 362 } 363for _ ,p := range m .packets { 364head := int (math .Floor (p .pos )) 365for i := range p .blocks { 366idx := head - i 367if idx < 0 || idx >= len (p .path ) { 368continue 369 } 370glyph ,hue := "▪" ,p .hue 371if i == 0 { 372glyph ,hue = "■" ,p .hue .mix (colors .hi ,0.35 ) 373 } 374c .put (p .path [idx ][0 ],p .path [idx ][1 ],glyph ,hue ) 375 } 376 } 377for _ ,f := range m .floats { 378rise := min (2 ,int (f .t * 2.2 )) 379c .put (f .x ,f .y - rise ,f .text ,f .hue .mix (colors .decor ,math .Min (1 ,f .t / 1.4 ))) 380 } 381}