v0.38.0 58133ea

interactive_queue_test.go RawHistoryFolder

1package modes
2
3import (
4	"context"
5	"strings"
6	"testing"
7
8	"github.com/bnema/zut/packages/core"
9	"github.com/bnema/zut/packages/provider"
10	"github.com/bnema/zut/packages/tui"
11)
12
13func TestQueuedMessageSummaryKeepsImageIndicatorWhenTextIsTruncated(t *testing.T) {
14	message := core.QueuedMessage{
15		Text:   strings.Repeat("x", 100),
16		Images: []provider.ImageBlock{{MimeType: "image/png", Data: []byte("png-1")}},
17	}
18
19	summary := queuedMessageSummary(message, 24)
20
21	if !strings.HasSuffix(summary, " [image]") {
22		t.Fatalf("summary = %q, want visible image indicator", summary)
23	}
24	if len([]rune(summary)) > 24 {
25		t.Fatalf("summary width = %d, want at most 24", len([]rune(summary)))
26	}
27}
28
29func TestBusySubmitQueuesClipboardImagePrompt(t *testing.T) {
30	agent := core.NewAgent(nil, "test-model", "", nil)
31	i := NewInteractive(InteractiveConfig{Agent: agent})
32	i.mu.Lock()
33	i.busy = true
34	i.mu.Unlock()
35	i.ed.SetValue("inspect [clipboard image #1]")
36	i.clipboardImages = []clipboardImageAttachment{testClipboardImage("[clipboard image #1]", "png-1")}
37
38	i.handleKey(context.Background(), tui.Key{Kind: tui.KeyEnter})
39
40	queued := agent.PendingQueuedMessages()
41	if len(queued) != 1 || queued[0].Text != "inspect" {
42		t.Fatalf("queued messages = %#v, want one inspect prompt", queued)
43	}
44	if len(queued[0].Images) != 1 || string(queued[0].Images[0].Data) != "png-1" {
45		t.Fatalf("queued images = %#v, want png-1", queued[0].Images)
46	}
47	if !i.ed.IsEmpty() || len(i.clipboardImages) != 0 {
48		t.Fatal("submitted image prompt remained in the editor")
49	}
50}
51
52func TestSlideBackRestoresQueuedImagesToEditor(t *testing.T) {
53	agent := core.NewAgent(nil, "test-model", "", nil)
54	image := testClipboardImage("unused", "png-1").Image
55	agent.QueueMessage("inspect", []provider.ImageBlock{image})
56	i := NewInteractive(InteractiveConfig{Agent: agent})
57	i.mu.Lock()
58	i.busy = true
59	i.mu.Unlock()
60
61	i.handleKey(context.Background(), tui.Key{Kind: tui.KeyUp, Alt: true})
62
63	if got := i.ed.Value(); got != "inspect [clipboard image #1]" {
64		t.Fatalf("editor = %q, want restored image marker", got)
65	}
66	if len(i.clipboardImages) != 1 || string(i.clipboardImages[0].Image.Data) != "png-1" {
67		t.Fatalf("clipboard images = %#v, want png-1", i.clipboardImages)
68	}
69}
70
71func TestEscapeRestoresMostRecentQueuedMessageToEditor(t *testing.T) {
72	agent := core.NewAgent(nil, "test-model", "", nil)
73	agent.QueueMessage("older follow-up", nil)
74	agent.QueueMessage("recover this draft", nil)
75
76	i := NewInteractive(InteractiveConfig{Agent: agent})
77	i.ed.SetValue("existing draft")
78	cancelled := 0
79	i.mu.Lock()
80	i.busy = true
81	i.cancelTurn = func() { cancelled++ }
82	i.mu.Unlock()
83
84	if done := i.handleKey(context.Background(), tui.Key{Kind: tui.KeyEsc}); done {
85		t.Fatal("Escape exited")
86	}
87	if cancelled != 1 {
88		t.Fatalf("cancelled = %d, want 1", cancelled)
89	}
90	if got, want := i.ed.Value(), "recover this draft"; got != want {
91		t.Errorf("editor = %q, want %q", got, want)
92	}
93	if got := agent.PendingQueuedMessages(); len(got) != 1 || got[0].Text != "older follow-up" {
94		t.Errorf("remaining queued messages = %v, want [older follow-up]", got)
95	}
96}
97
98func TestEscapeRestoresHostQueuedMessageToEditor(t *testing.T) {
99	i := NewInteractive(InteractiveConfig{})
100	cancelled := 0
101	i.mu.Lock()
102	i.busy = true
103	i.cancelTurn = func() { cancelled++ }
104	i.queued = []core.QueuedMessage{{Text: "recover this draft"}}
105	i.mu.Unlock()
106
107	i.handleKey(context.Background(), tui.Key{Kind: tui.KeyEsc})
108
109	if cancelled != 1 {
110		t.Fatalf("cancelled = %d, want 1", cancelled)
111	}
112	if got, want := i.ed.Value(), "recover this draft"; got != want {
113		t.Errorf("editor = %q, want %q", got, want)
114	}
115}

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