v0.38.0 58133ea

model_dialog_test.go RawHistoryFolder

1package modes
2
3import (
4	"context"
5	"strings"
6	"testing"
7
8	"github.com/bnema/zut/packages/provider"
9	"github.com/bnema/zut/packages/tui"
10)
11
12func TestModelDialogReasoningNavigation(t *testing.T) {
13	d := &modelDialog{
14		active:        true,
15		view:          []provider.Model{{Provider: "openai-codex", ID: "gpt-5.6-luna", Reasoning: true}},
16		reasoning:     "high",
17		showReasoning: true,
18	}
19
20	act := d.HandleKey(tui.Key{Kind: tui.KeyRight})
21	if !act.ReasoningChanged || act.Reasoning != "xhigh" || d.reasoning != "xhigh" {
22		t.Fatalf("right action = %+v, dialog reasoning = %q; want xhigh", act, d.reasoning)
23	}
24	act = d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'L'})
25	if !act.ReasoningChanged || act.Reasoning != "max" || d.reasoning != "max" {
26		t.Fatalf("L action = %+v, dialog reasoning = %q; want max", act, d.reasoning)
27	}
28	act = d.HandleKey(tui.Key{Kind: tui.KeyLeft})
29	if !act.ReasoningChanged || act.Reasoning != "xhigh" {
30		t.Fatalf("left action = %+v; want xhigh", act)
31	}
32	act = d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'H'})
33	if !act.ReasoningChanged || act.Reasoning != "high" {
34		t.Fatalf("H action = %+v; want high", act)
35	}
36}
37
38func TestModelDialogReasoningAllowsLowercaseFilter(t *testing.T) {
39	d := &modelDialog{active: true, showReasoning: true}
40	for _, r := range "claude" {
41		d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: r})
42	}
43	if d.query != "claude" {
44		t.Fatalf("query = %q, want claude", d.query)
45	}
46}
47
48func TestModelDialogWithoutReasoningTreatsHAsFilter(t *testing.T) {
49	d := &modelDialog{active: true}
50	d.HandleKey(tui.Key{Kind: tui.KeyRune, Rune: 'H'})
51	if d.query != "H" {
52		t.Fatalf("embedded picker query = %q, want H", d.query)
53	}
54}
55
56func TestModelDialogReasoningNavigationStopsAtBounds(t *testing.T) {
57	d := &modelDialog{
58		active:        true,
59		showReasoning: true,
60		view:          []provider.Model{{Provider: "openai-codex", ID: "gpt-5.6-luna", Reasoning: true}},
61		reasoning:     "",
62	}
63
64	if act := d.HandleKey(tui.Key{Kind: tui.KeyLeft}); act.ReasoningChanged {
65		t.Fatalf("left at off changed reasoning: %+v", act)
66	}
67	d.reasoning = "max"
68	if act := d.HandleKey(tui.Key{Kind: tui.KeyRight}); act.ReasoningChanged {
69		t.Fatalf("right at max changed reasoning: %+v", act)
70	}
71}
72
73func TestModelDialogReasoningNavigationAppliesToInteractive(t *testing.T) {
74	d := &modelDialog{
75		active:        true,
76		showReasoning: true,
77		view:          []provider.Model{{Provider: "openai-codex", ID: "gpt-5.6-luna", Reasoning: true}},
78		reasoning:     "high",
79	}
80	var changed string
81	i := &Interactive{
82		cfg: InteractiveConfig{
83			Reasoning:          "high",
84			OnReasoningChanged: func(level string) { changed = level },
85		},
86		modelDialog: d,
87	}
88
89	i.handleKey(context.Background(), tui.Key{Kind: tui.KeyRight})
90	if i.cfg.Reasoning != "xhigh" || changed != "xhigh" {
91		t.Fatalf("reasoning = %q, callback = %q; want xhigh", i.cfg.Reasoning, changed)
92	}
93}
94
95func TestModelDialogWithoutReasoningIgnoresArrows(t *testing.T) {
96	d := &modelDialog{
97		active:    true,
98		view:      []provider.Model{{Provider: "openai-codex", ID: "gpt-5.6-luna", Reasoning: true}},
99		reasoning: "high",
100	}
101	if act := d.HandleKey(tui.Key{Kind: tui.KeyRight}); act.ReasoningChanged || d.reasoning != "high" {
102		t.Fatalf("hidden reasoning changed: action=%+v reasoning=%q", act, d.reasoning)
103	}
104}
105
106func TestModelDialogReasoningReflectsHighlightedModel(t *testing.T) {
107	d := &modelDialog{
108		active:        true,
109		showReasoning: true,
110		reasoning:     "high",
111		view:          []provider.Model{{Provider: "google", ID: "gemini-2.0-flash"}},
112	}
113	text := stripANSIBytes(strings.Join(d.Render(tui.Dark, 100), "\n"))
114	if !strings.Contains(text, "reasoning: off") {
115		t.Fatalf("effective reasoning level missing from %q", text)
116	}
117	if !strings.Contains(text, "unavailable for highlighted model") {
118		t.Fatalf("unsupported-model hint missing from %q", text)
119	}
120	if strings.Contains(text, "H/L to change") {
121		t.Fatalf("unsupported model advertises reasoning controls: %q", text)
122	}
123}
124
125func TestModelDialogReasoningUsesLevelColor(t *testing.T) {
126	tests := []struct {
127		level string
128		color tui.TerminalColor
129	}{
130		{"off", tui.Dark.Muted},
131		{"low", tui.Dark.Accent},
132		{"high", tui.Dark.Warning},
133		{"max", tui.Dark.ThinkingMax},
134	}
135	for _, tt := range tests {
136		if got := reasoningLevelColor(tui.Dark, tt.level); got != tt.color {
137			t.Errorf("reasoningLevelColor(%q) = %#v, want %#v", tt.level, got, tt.color)
138		}
139	}
140
141	d := &modelDialog{
142		active:        true,
143		showReasoning: true,
144		reasoning:     "max",
145		view:          []provider.Model{{Provider: "openai-codex", ID: "gpt-5.6-luna", Reasoning: true}},
146	}
147	text := strings.Join(d.Render(tui.Dark, 100), "\n")
148	if !strings.Contains(text, tui.Dark.FGColor(tui.Dark.ThinkingMax, "max")) {
149		t.Fatalf("max reasoning color missing from %q", text)
150	}
151	if !strings.Contains(text, "←/→ or H/L to change") {
152		t.Fatalf("reasoning navigation hint missing from %q", text)
153	}
154}

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