okami.mom/albedo

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

Aly Raffauffeat(progress): reduce preview parsing from O(s*n^2) to O(n)60cce3a

main
5.2 KiB211 linesraw
1package tui
2
3import (
4	"albedo/cli/internal/daemon"
5	"iter"
6	"slices"
7	"strings"
8	"unicode/utf8"
9)
10
11const (
12	agentPreviewBytes = 64 << 10
13	agentPreviewLines = 120
14)
15
16type agentTailSpan struct {
17	start, end uint64
18	kind       tailKind
19}
20
21// agentTail owns its bytes. Absolute offsets let a long line lose its oldest
22// bytes without shifting the window or retaining the original stream chunks.
23type agentTail struct {
24	data    []byte
25	lines   [agentPreviewLines]agentTailSpan
26	end     uint64
27	head    int
28	count   int
29	open    bool
30	omitted bool
31}
32
33func (tail *agentTail) dropOldest() {
34	tail.lines[tail.head] = agentTailSpan{}
35	tail.head = (tail.head + 1) % agentPreviewLines
36	tail.count--
37	tail.omitted = true
38}
39
40func (tail *agentTail) begin(kind tailKind) {
41	if tail.count == agentPreviewLines {
42		tail.dropOldest()
43	}
44	index := (tail.head + tail.count) % agentPreviewLines
45	tail.lines[index] = agentTailSpan{start: tail.end, end: tail.end, kind: kind}
46	tail.count++
47	tail.open = true
48}
49
50func (tail *agentTail) grow(extra int) {
51	start := tail.lines[tail.head].start
52	retained := int(tail.end - start)
53	required := min(agentPreviewBytes, retained+extra)
54	if required <= len(tail.data) {
55		return
56	}
57	data := make([]byte, min(agentPreviewBytes, max(64, required, len(tail.data)*2)))
58	if retained > 0 {
59		index := int(start % uint64(len(tail.data)))
60		copied := copy(data[:retained], tail.data[index:])
61		copy(data[copied:retained], tail.data[:retained-copied])
62	}
63	for offset := range tail.count {
64		span := &tail.lines[(tail.head+offset)%agentPreviewLines]
65		span.start -= start
66		span.end -= start
67	}
68	tail.end -= start
69	tail.data = data
70}
71
72func (tail *agentTail) appendText(text string) {
73	if len(text) == 0 {
74		return
75	}
76	tail.grow(len(text))
77	capacity := uint64(len(tail.data))
78	next := tail.end + uint64(len(text))
79	if len(text) > len(tail.data) {
80		text = text[len(text)-len(tail.data):]
81	}
82	index := int((next - uint64(len(text))) % capacity)
83	copied := copy(tail.data[index:], text)
84	copy(tail.data, text[copied:])
85	tail.end = next
86	tail.lines[(tail.head+tail.count-1)%agentPreviewLines].end = next
87	if next <= capacity {
88		return
89	}
90	oldest := next - capacity
91	for tail.count > 1 && tail.lines[tail.head].end <= oldest {
92		tail.dropOldest()
93	}
94	first := &tail.lines[tail.head]
95	if first.start < oldest {
96		first.start = oldest
97		for first.start < first.end && !utf8.RuneStart(tail.data[first.start%capacity]) {
98			first.start++
99		}
100		tail.omitted = true
101	}
102}
103
104func (tail *agentTail) write(text string, kind tailKind) {
105	for {
106		line, rest, newline := strings.Cut(text, "\n")
107		if !tail.open {
108			tail.begin(kind)
109		}
110		tail.appendText(line)
111		if !newline {
112			return
113		}
114		tail.open = false
115		text = rest
116	}
117}
118
119func (tail *agentTail) push(line tailLine) {
120	tail.open = false
121	tail.write(strings.TrimRight(line.text, " "), line.kind)
122	tail.open = false
123}
124
125func (tail *agentTail) text(span agentTailSpan) string {
126	if span.start == span.end {
127		return ""
128	}
129	var text strings.Builder
130	text.Grow(int(span.end - span.start))
131	start := int(span.start % uint64(len(tail.data)))
132	length := int(span.end - span.start)
133	first := min(length, len(tail.data)-start)
134	text.Write(tail.data[start : start+first])
135	text.Write(tail.data[:length-first])
136	return text.String()
137}
138
139// newest yields owned strings, so callers may retain them after a reset.
140func (tail *agentTail) newest(trimNewlines bool) iter.Seq[tailLine] {
141	return func(yield func(tailLine) bool) {
142		trimming := trimNewlines
143		for offset := tail.count - 1; offset >= 0; offset-- {
144			span := tail.lines[(tail.head+offset)%agentPreviewLines]
145			if trimming && span.start == span.end && offset > 0 {
146				continue
147			}
148			trimming = false
149			if !yield(tailLine{kind: span.kind, text: tail.text(span)}) {
150				return
151			}
152		}
153	}
154}
155
156// setCode replaces the preview window supplied by the daemon.
157func (tail *agentTail) setCode(code *daemon.ToolCodePreview) {
158	*tail = agentTail{}
159	if code != nil {
160		tail.write(strings.ReplaceAll(code.Text, "\t", "    "), tailCode)
161	}
162}
163
164type agentTailCache struct {
165	node     *agentNode
166	ink      ink
167	rows     []string
168	revision uint64
169	width    int
170	height   int
171}
172
173// refreshTail runs after updates, never from View. Only the selected node
174// needs wrapped rows; animation-only frames reuse the previous result.
175func (m *AgentsViewModel) refreshTail() {
176	node := m.nodes[m.selected]
177	width, height := m.paneWidth()-1, max(0, m.Height)
178	cache := &m.tailCache
179	if node == nil || width <= 0 || height == 0 {
180		*cache = agentTailCache{}
181		return
182	}
183	if cache.node == node && cache.revision == node.revision && cache.width == width && cache.height == height && cache.ink == transcriptInk {
184		return
185	}
186	rows := make([]string, 0, height)
187	appendLine := func(line tailLine) bool {
188		wrapped := drawTail(line, width, height-len(rows))
189		for _, row := range slices.Backward(wrapped) {
190			rows = append(rows, row)
191		}
192		return len(rows) < height
193	}
194	for line := range node.preview.newest(node.lineKind == tailCode) {
195		if line.text == "" && node.lineKind != tailCode {
196			continue
197		}
198		if !appendLine(line) {
199			break
200		}
201	}
202	if len(rows) < height {
203		for line := range node.tail.newest(false) {
204			if !appendLine(line) {
205				break
206			}
207		}
208	}
209	slices.Reverse(rows)
210	*cache = agentTailCache{node: node, rows: rows, ink: transcriptInk, revision: node.revision, width: width, height: height}
211}