v0.9.1 406ddf4

context_budget.go RawHistoryFolder

1package core
2
3import (
4	"unicode/utf8"
5
6	"github.com/bnema/zut/packages/provider"
7)
8
9const (
10	maxToolResultTextBytes      = 32 * 1024
11	maxToolResultTotalTextBytes = 128 * 1024
12	toolResultOmissionMarker    = "[tool result content omitted]"
13)
14
15// projectToolResultMessages returns a copied provider-input view of msgs.
16// Text inside each tool result is bounded independently and then against the
17// total budget, with newer results receiving the remaining budget first.
18// Tool-result blocks and all non-text content remain in the projection so
19// tool-call/result pairing stays valid.
20func projectToolResultMessages(msgs []provider.Message) []provider.Message {
21	projected := copyToolResultMessages(msgs)
22	remaining := maxToolResultTotalTextBytes
23
24	for i := len(projected) - 1; i >= 0; i-- {
25		for j := len(projected[i].Content) - 1; j >= 0; j-- {
26			result, ok := projected[i].Content[j].(provider.ToolResultBlock)
27			if !ok {
28				continue
29			}
30
31			textBytes := toolResultTextBytes(result.Content)
32			if textBytes == 0 {
33				continue
34			}
35
36			limit := maxToolResultTextBytes
37			if remaining < limit {
38				limit = remaining
39			}
40			var retained int
41			result.Content, retained = projectToolResultContent(result.Content, limit)
42			projected[i].Content[j] = result
43			remaining -= retained
44		}
45	}
46
47	return projected
48}
49
50// copyToolResultMessages copies the slices that the projection can replace.
51// Content blocks are values, so sharing their immutable fields is safe; the
52// nested content slice of every ToolResultBlock is copied before projection.
53func copyToolResultMessages(msgs []provider.Message) []provider.Message {
54	if msgs == nil {
55		return nil
56	}
57	out := make([]provider.Message, len(msgs))
58	for i, message := range msgs {
59		out[i] = message
60		if message.Content != nil {
61			out[i].Content = append([]provider.Content(nil), message.Content...)
62		}
63		if message.AddedToolNames != nil {
64			out[i].AddedToolNames = append([]string(nil), message.AddedToolNames...)
65		}
66		if message.Meta != nil {
67			out[i].Meta = make(map[string]string, len(message.Meta))
68			for key, value := range message.Meta {
69				out[i].Meta[key] = value
70			}
71		}
72		for j, content := range out[i].Content {
73			if result, ok := content.(provider.ToolResultBlock); ok {
74				result.Content = append([]provider.Content(nil), result.Content...)
75				out[i].Content[j] = result
76			}
77		}
78	}
79	return out
80}
81
82func toolResultTextBytes(content []provider.Content) int {
83	total := 0
84	for _, block := range content {
85		if text, ok := block.(provider.TextBlock); ok {
86			total += len(text.Text)
87		}
88	}
89	return total
90}
91
92// projectToolResultContent projects one result's textual content to limit
93// bytes. Every truncated result receives an omission marker, even after the
94// aggregate content budget is exhausted. The returned count excludes that
95// marker so the budget covers retained tool output rather than its notices.
96// Non-text blocks are retained in their original order.
97func projectToolResultContent(content []provider.Content, limit int) ([]provider.Content, int) {
98	textBytes := toolResultTextBytes(content)
99	if textBytes <= limit {
100		return append([]provider.Content(nil), content...), textBytes
101	}
102
103	out := make([]provider.Content, 0, len(content)+1)
104	prefixRemaining := limit
105	prefixUsed := 0
106	truncated := false
107	for _, block := range content {
108		text, ok := block.(provider.TextBlock)
109		if !ok {
110			out = append(out, block)
111			continue
112		}
113		if truncated {
114			continue
115		}
116		if len(text.Text) <= prefixRemaining {
117			out = append(out, text)
118			prefixRemaining -= len(text.Text)
119			prefixUsed += len(text.Text)
120			continue
121		}
122
123		text.Text = utf8Prefix(text.Text, prefixRemaining)
124		if text.Text != "" {
125			out = append(out, text)
126		}
127		prefixUsed += len(text.Text)
128		truncated = true
129	}
130	out = append(out, provider.TextBlock{Text: toolResultOmissionMarker})
131	return out, prefixUsed
132}
133
134func utf8Prefix(text string, limit int) string {
135	if limit <= 0 {
136		return ""
137	}
138	if limit >= len(text) {
139		return text
140	}
141	for limit > 0 && !utf8.RuneStart(text[limit]) {
142		limit--
143	}
144	return text[:limit]
145}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close