Something went wrong. Try again.
small gleam coding and persistent agent daemon with a detachable cli and a canvas ui forked from okami.mom/albedo
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389package tui
import ( "fmt" "math" "slices" "strings" "time"
"github.com/charmbracelet/x/ansi")
type agentPacket struct { to string edge string path [][2]int hue rgb pos float64 blocks int tokens int}
type agentFloat struct { text string hue rgb x, y int t float64}
func (m *AgentsViewModel) dagWidth() int { w := m.paneWidth() if w > 0 { return m.Width - w - 2 } return m.Width - 1}
func (m *AgentsViewModel) paneWidth() int { if m.Width < paneMinWidth { return 0 } return min(46, max(36, m.Width/3))}
// layout places dots by depth, each depth wrapped into rows that fit the// pane, parents before their children so a family stays together.func (m *AgentsViewModel) layout() { // The app sizes every screen, including this one before it first opens. if m.nodes == nil { return } you := m.node(agentsYou, agentsYou) you.x, you.y = 2, 2 children := map[string][]string{} var roots, peers []string for id, n := range m.nodes { switch { case id == agentsYou: case n.peer: peers = append(peers, id) case n.parent == "" || m.nodes[n.parent] == nil: roots = append(roots, id) default: children[n.parent] = append(children[n.parent], id) } } for _, list := range children { slices.Sort(list) } slices.Sort(roots) slices.Sort(peers) levels := [][]string{append(roots, peers...)} for { var next []string for _, id := range levels[len(levels)-1] { next = append(next, children[id]...) } if len(next) == 0 { break } levels = append(levels, next) } width := max(agentsSlotW, m.dagWidth()-4) perRow := max(1, width/agentsSlotW) y := 5 m.order = m.order[:0] for _, level := range levels { for start := 0; start < len(level); start += perRow { row := level[start:min(len(level), start+perRow)] used := len(row) * agentsSlotW x := 2 + max(0, (width-used)/2) for i, id := range row { n := m.nodes[id] n.x, n.y = x+i*agentsSlotW, y m.order = append(m.order, id) } y += 3 } y += agentsLevelGp } if m.nodes[m.selected] == nil && len(m.order) > 0 { m.selected = m.order[0] }}
// route runs from a to b: down from a, across the row above b, into b. A// route upward is the same route reversed.func route(a, b *agentNode) [][2]int { if b.y < a.y { cells := route(b, a) slices.Reverse(cells) return cells } var cells [][2]int channel := b.y - 1 if b.y == a.y { channel = a.y - 1 cells = append(cells, [2]int{a.x, a.y - 1}) } for r := a.y + 1; r <= channel; r++ { cells = append(cells, [2]int{a.x, r}) } step := 1 if b.x < a.x { step = -1 } for c := a.x + step; c != b.x+step; c += step { cells = append(cells, [2]int{c, channel}) } return cells}
func edgeKey(a, b string) string { return a + ">" + b }
func (m *AgentsViewModel) send(from, to string, tokens int) { a, b := m.nodes[from], m.nodes[to] if a == nil || b == nil || from == to { return } path := route(a, b) if len(path) == 0 { return } blocks := max(1, min(5, int(math.Round(math.Log2(float64(tokens)/200+1))))) var key string if a.parent == to { key = edgeKey(to, from) } else { key = edgeKey(from, to) } m.packets = append(m.packets, agentPacket{path: path, pos: -float64(blocks), blocks: blocks, hue: a.hue, to: to, tokens: tokens, edge: key}) m.heat[key] = 1}
// step advances one frame of animation.func (m *AgentsViewModel) step() { now := time.Now() dt := math.Min(0.2, now.Sub(m.last).Seconds()) m.last = now m.clock += dt m.noticeT = math.Max(0, m.noticeT-dt) live := m.packets[:0] for _, p := range m.packets { p.pos += 30 * dt m.heat[p.edge] = 1 if p.pos-float64(p.blocks) >= float64(len(p.path)-1) { if n := m.nodes[p.to]; n != nil { n.flash = 1 m.floats = append(m.floats, agentFloat{x: n.x + 2, y: n.y - 1, text: "▴" + compactCount(p.tokens), hue: p.hue}) } continue } live = append(live, p) } m.packets = live for key, h := range m.heat { if h -= dt * 0.8; h <= 0 { delete(m.heat, key) } else { m.heat[key] = h } } // an edge still carrying a packet stays hot for _, p := range m.packets { m.heat[p.edge] = 1 } floats := m.floats[:0] for _, f := range m.floats { f.t += dt if f.t < 1.4 { floats = append(floats, f) } } m.floats = floats for _, n := range m.nodes { n.flash = math.Max(0, n.flash-dt*2.5) n.rate *= math.Exp(-dt * 1.5) }}
// moving is whether the next frame can differ from this one: running agents// pulse, and packets, heat, floats, flashes, and notices play out.func (m AgentsViewModel) moving() bool { if len(m.packets) > 0 || len(m.heat) > 0 || len(m.floats) > 0 || m.noticeT > 0 { return true } for _, n := range m.nodes { if n.running || n.flash > 0 { return true } } return false}
func compactCount(n int) string { if n >= 1000 { return fmt.Sprintf("%.1fk", float64(n)/1000) } return fmt.Sprintf("%d", n)}
var agentGlyphs = map[int]rune{5: '│', 10: '─', 6: '╭', 12: '╮', 3: '╰', 9: '╯', 7: '├', 13: '┤', 14: '┬', 11: '┴', 15: '┼', 1: '│', 4: '│', 2: '─', 8: '─'}
func (m *AgentsViewModel) drawEdges(c *agentCanvas) { type mark struct { mask int heat float64 hue rgb dotted bool } marks := map[[2]int]*mark{} // dir is one bit per compass direction; a cell's mask names the arms meeting in it. dir := func(dx, dy int) int { switch { case dy < 0: return 1 case dy > 0: return 4 case dx > 0: return 2 default: return 8 } } draw := func(from, to *agentNode, heat float64, dotted bool) { cells := append([][2]int{{from.x, from.y}}, route(from, to)...) for i := 1; i < len(cells); i++ { cell, prev := cells[i], cells[i-1] mk := marks[cell] if mk == nil { mk = &mark{hue: from.hue} marks[cell] = mk } next := [2]int{to.x, to.y} if i+1 < len(cells) { next = cells[i+1] } mk.mask |= dir(prev[0]-cell[0], prev[1]-cell[1]) | dir(next[0]-cell[0], next[1]-cell[1]) if heat >= mk.heat { mk.heat, mk.hue = heat, from.hue } mk.dotted = mk.dotted || dotted } } crowded := len(m.nodes) > 24 for _, n := range m.nodes { parent := m.nodes[n.parent] if parent == nil || n.peer { continue } heat := m.heat[edgeKey(n.parent, n.id)] if crowded && heat == 0 { continue } draw(parent, n, heat, false) } if root := m.nodes[m.root]; root != nil { draw(m.nodes[agentsYou], root, m.heat[edgeKey(agentsYou, m.root)]+m.heat[edgeKey(m.root, agentsYou)], false) } for key, heat := range m.heat { from, to, _ := strings.Cut(key, ">") a, b := m.nodes[from], m.nodes[to] if a == nil || b == nil || b.parent == from || (from == agentsYou && to == m.root) || (to == agentsYou && from == m.root) { continue } draw(a, b, heat, true) } decor := agentColors().decor for cell, mk := range marks { g := agentGlyphs[mk.mask] if g == 0 { g = '·' } if mk.dotted { switch { case mk.mask&5 != 0 && mk.mask&10 == 0: g = '┆' case mk.mask&10 != 0 && mk.mask&5 == 0: g = '┄' } } c.put(cell[0], cell[1], string(g), decor.mix(mk.hue, math.Min(1, mk.heat*1.1))) }}
func (m *AgentsViewModel) drawNodes(c *agentCanvas) { t, colors := m.clock, agentColors() for id, n := range m.nodes { glyph := "●" var hue rgb switch { case id == agentsYou: glyph, hue = "◆", colors.you case n.running: p := 0.5 + 0.5*math.Sin(t*5+n.phase) if p > 0.55 { glyph = "◉" } hue = n.hue.mix(colors.faint, 0.2).mix(n.hue, p) case n.closed: glyph, hue = "✓", n.hue.mix(colors.faint, 0.5) default: glyph = "○" if n.peer { glyph = "◇" } hue = n.hue.mix(colors.faint, 0.3) } if n.flash > 0 { hue = hue.mix(colors.hi, n.flash*0.7) } c.put(n.x, n.y, glyph, hue) name := ansi.Truncate(n.name, agentsSlotW-4, "…") label := n.hue.mix(colors.hi, 0.35) if id == m.selected { label = colors.hi c.put(n.x-1, n.y, "[", n.hue) c.put(n.x+2+ansi.StringWidth(name), n.y, "]", n.hue) } c.put(n.x+2, n.y, name, label) if id == agentsYou { switch { case m.root != "": case m.err != nil: c.put(n.x, n.y+2, "could not load agents", colors.faint) default: c.put(n.x, n.y+2, "loading agents…", colors.faint) } continue } // The jam: five bars riding the token rate. var bars strings.Builder level := math.Min(1, n.rate/400) for i := range 5 { v := 0.0 if n.running { v = 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)) } bars.WriteRune(agentsBars[max(0, min(7, int(math.Round(v*7))))]) } barHue := colors.decor if n.running { barHue = n.hue } c.put(n.x+2, n.y+1, bars.String(), barHue) if n.chars > 0 { c.put(n.x+8, n.y+1, compactCount(n.chars/4), colors.faint) } } for _, p := range m.packets { head := int(math.Floor(p.pos)) for i := range p.blocks { idx := head - i if idx < 0 || idx >= len(p.path) { continue } glyph, hue := "▪", p.hue if i == 0 { glyph, hue = "■", p.hue.mix(colors.hi, 0.35) } c.put(p.path[idx][0], p.path[idx][1], glyph, hue) } } for _, f := range m.floats { rise := min(2, int(f.t*2.2)) c.put(f.x, f.y-rise, f.text, f.hue.mix(colors.decor, math.Min(1, f.t/1.4))) }}