v0.50.1 a637010

skill_pins_recall_test.go RawHistoryFolder

1package modes
2
3import (
4	"context"
5	"strings"
6	"sync"
7	"testing"
8
9	"github.com/bnema/zut/packages/agent/skills"
10	"github.com/bnema/zut/packages/core"
11	"github.com/bnema/zut/packages/provider"
12	"github.com/bnema/zut/packages/tui"
13)
14
15func newPinnedRecallInteractive(t *testing.T) *Interactive {
16	t.Helper()
17	return NewInteractive(InteractiveConfig{
18		Agent: core.NewAgent(nil, "test-model", "", nil), CWD: t.TempDir(),
19		LoadSkillPins: func(string) (skills.Pins, error) { return skills.Pins{Global: []string{"review"}}, nil },
20		SkillSnapshot: reviewSnapshot, PreloadPinnedSkills: true,
21	})
22}
23
24func TestPinnedQueuedDraftRecallKeepsEditorRawAndRearms(t *testing.T) {
25	for name, key := range map[string]tui.Key{"alt-up": {Kind: tui.KeyUp, Alt: true}, "escape": {Kind: tui.KeyEsc}} {
26		t.Run(name, func(t *testing.T) {
27			i := newPinnedRecallInteractive(t)
28			expanded := i.consumePinnedSkills("user draft")
29			i.agent.QueuePrompt(core.QueuedMessage{Text: expanded})
30			i.mu.Lock()
31			i.busy = true
32			i.cancelTurn = func() {}
33			i.mu.Unlock()
34			i.handleKey(context.Background(), key)
35			if text := i.ed.Value(); text != "user draft" {
36				t.Fatalf("recalled editor text=%q, want raw draft", text)
37			}
38			if next := i.consumePinnedSkills(i.ed.Value()); strings.Count(next, "Inspect carefully.") != 1 {
39				t.Fatalf("recall lost or doubled pins: %q", next)
40			}
41		})
42	}
43}
44
45func TestDiscardPinnedQueuedDraftRearmsNextUser(t *testing.T) {
46	for _, preserveHost := range []bool{false, true} {
47		i := newPinnedRecallInteractive(t)
48		expanded := i.consumePinnedSkills("discarded draft")
49		i.agent.QueuePrompt(core.QueuedMessage{Text: expanded})
50		i.agent.QueuePrompt(core.QueuedMessage{Text: "host report", HostEvent: true})
51		i.mu.Lock()
52		i.discardQueuedMessagesLocked(preserveHost)
53		i.mu.Unlock()
54		if next := i.consumePinnedSkills("new draft"); strings.Count(next, "Inspect carefully.") != 1 || !strings.HasSuffix(next, "new draft") {
55			t.Fatalf("discard lost pins: %q", next)
56		}
57	}
58}
59
60func TestCtrlCClearsPinnedQueueWithoutLosingPreload(t *testing.T) {
61	i := newPinnedRecallInteractive(t)
62	expanded := i.consumePinnedSkills("discarded draft")
63	i.agent.QueuePrompt(core.QueuedMessage{Text: expanded})
64	i.handleKey(context.Background(), tui.Key{Kind: tui.KeyCtrlC})
65	if next := i.consumePinnedSkills("next draft"); strings.Count(next, "Inspect carefully.") != 1 {
66		t.Fatalf("clearing queue lost pins: %q", next)
67	}
68}
69
70func TestPinnedRecallAndHistorySynchronizeWithHostArming(t *testing.T) {
71	i := newPinnedRecallInteractive(t)
72	expanded := i.consumePinnedSkills("user draft")
73	i.agent.SetMessages([]provider.Message{{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: expanded}}}})
74	var wg sync.WaitGroup
75	wg.Add(1)
76	go func() {
77		defer wg.Done()
78		for range 100 {
79			i.SetPinnedSkillsPending(true)
80		}
81	}()
82	for range 100 {
83		i.restoreQueuedMessageToEditor(core.QueuedMessage{Text: expanded})
84		i.inputHistory()
85	}
86	wg.Wait()
87}
88
89func TestPinnedInputHistoryShowsRawUserText(t *testing.T) {
90	i := newPinnedRecallInteractive(t)
91	expanded := i.consumePinnedSkills("user draft")
92	i.agent.SetMessages([]provider.Message{{Role: provider.RoleUser, Content: []provider.Content{provider.TextBlock{Text: expanded}}}})
93	if history := i.inputHistory(); len(history) != 1 || history[0] != "user draft" {
94		t.Fatalf("input history contains injected bodies: %#v", history)
95	}
96	if got := i.skillPins.rawPrompt("Pinned skills for this conversation.\nuser-written text"); !strings.Contains(got, "user-written text") {
97		t.Fatal("stripped unrelated user-written preamble")
98	}
99}

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