okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

nekomimi.pettui: pace live reply redraws, cache settled markdown, draw only visible picker rows, drop per-event window reads6e2130b

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