v0.17.0 949637b

view_cache_test.go RawHistoryFolder

1package tui
2
3import (
4	"fmt"
5	"strings"
6	"testing"
7
8	"github.com/bnema/zut/packages/provider"
9)
10
11func TestViewCacheInvalidatesToolOutputExpansionAndWidth(t *testing.T) {
12	messages := []provider.Message{
13		{
14			Role: provider.RoleAssistant,
15			Content: []provider.Content{provider.ToolCallBlock{
16				ID: "call-1", Name: "bash", Arguments: []byte(`{"command":"cat file"}`),
17			}},
18		},
19		{
20			Role: provider.RoleTool,
21			Content: []provider.Content{provider.ToolResultBlock{
22				CallID:  "call-1",
23				Content: []provider.Content{provider.TextBlock{Text: cacheTestResult("old")}},
24			}},
25		},
26	}
27	view := &View{Theme: Dark, Messages: messages, MessagesRevision: 1}
28	collapsed, _ := view.BuildWithAnchors(40)
29	if len(collapsed) == 0 || !strings.Contains(strings.Join(collapsed, "\n"), "more lines") {
30		t.Fatal("long tool result did not use the collapsed presentation")
31	}
32
33	view.ExpandAll = true
34	expanded, _ := view.BuildWithAnchors(40)
35	if len(expanded) <= len(collapsed) {
36		t.Fatalf("expansion did not invalidate rows: collapsed=%d expanded=%d", len(collapsed), len(expanded))
37	}
38
39	wide, _ := view.BuildWithAnchors(120)
40	// The wide render may have the same row count for this fixture, but it
41	// must still be rebuilt for the new wrapping width.
42	if strings.Join(wide, "\n") == strings.Join(expanded, "\n") {
43		t.Fatal("resize reused rows from the old width")
44	}
45
46	view.ExpandAll = false
47	view.Messages[1].Content = []provider.Content{provider.ToolResultBlock{
48		CallID:  "call-1",
49		Content: []provider.Content{provider.TextBlock{Text: cacheTestResult("new marker")}},
50	}}
51	view.Messages = append(view.Messages, provider.Message{
52		Role:    provider.RoleUser,
53		Content: []provider.Content{provider.TextBlock{Text: "third message"}},
54	})
55	view.MessagesRevision = 2
56	updated, _ := view.BuildWithAnchors(40)
57	if !strings.Contains(strings.Join(updated, "\n"), "new marker") {
58		t.Fatal("tool output revision reused stale cached rows")
59	}
60}
61
62func TestViewRevisionCacheIsBoundToTailLimit(t *testing.T) {
63	messages := make([]provider.Message, 20)
64	for i := range messages {
65		messages[i] = provider.Message{
66			Role:    provider.RoleUser,
67			Content: []provider.Content{provider.TextBlock{Text: fmt.Sprintf("message %d", i)}},
68		}
69	}
70	view := &View{Theme: Dark, Messages: messages, MessagesRevision: 1, TailLimit: 5}
71	view.Build(80)
72	if view.messageCacheStart != len(messages)-view.TailLimit {
73		t.Fatalf("message cache start=%d, want %d", view.messageCacheStart, len(messages)-view.TailLimit)
74	}
75	if len(view.messageCache) > view.TailLimit {
76		t.Fatalf("message cache length=%d exceeds tail limit %d", len(view.messageCache), view.TailLimit)
77	}
78
79	view.Build(80)
80	if len(view.messageCache) > view.TailLimit {
81		t.Fatalf("message cache grew on a cache hit: length=%d", len(view.messageCache))
82	}
83}
84
85func TestRenderLiveToolResultCollapseUsesRenderedLineCount(t *testing.T) {
86	var text strings.Builder
87	for i := 0; i < 13; i++ {
88		text.WriteString(strings.Repeat("x", 17))
89		text.WriteByte('\n')
90	}
91	view := &View{Theme: Dark}
92	full := view.renderToolText(text.String(), 20, Dark.ToolOut, "", 1)
93	collapsed := view.renderLiveToolResult(text.String(), 20, Dark.ToolOut, "")
94	joined := strings.Join(collapsed, "\n")
95	wantTotal := fmt.Sprintf("%d total", len(full))
96	if !strings.Contains(joined, wantTotal) {
97		t.Fatalf("collapse footer omitted rendered total %q: %s", wantTotal, joined)
98	}
99}
100
101func cacheTestResult(prefix string) string {
102	var b strings.Builder
103	for n := 0; n < 32; n++ {
104		b.WriteString(prefix)
105		b.WriteString(" line ")
106		b.WriteString(string(rune('a' + n%26)))
107		b.WriteByte('\n')
108	}
109	return b.String()
110}

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