v0.3.39 be11074

btw_dialog_test.go RawHistoryFolder

1package modes
2
3import (
4	"context"
5	"encoding/json"
6	"strings"
7	"sync"
8	"sync/atomic"
9	"testing"
10	"time"
11
12	"github.com/patriceckhart/zot/packages/core"
13	"github.com/patriceckhart/zot/packages/provider"
14	"github.com/patriceckhart/zot/packages/tui"
15)
16
17type btwToolTestClient struct {
18	mu       sync.Mutex
19	requests []provider.Request
20	done     chan struct{}
21}
22
23func (c *btwToolTestClient) Name() string { return "btw-test" }
24
25func (c *btwToolTestClient) Stream(_ context.Context, req provider.Request) (<-chan provider.Event, error) {
26	c.mu.Lock()
27	c.requests = append(c.requests, req)
28	call := len(c.requests)
29	c.mu.Unlock()
30
31	out := make(chan provider.Event, 4)
32	go func() {
33		defer close(out)
34		if call == 1 {
35			out <- provider.EventToolStart{ID: "tool-1", Name: "echo"}
36			out <- provider.EventToolArgs{ID: "tool-1", Delta: `{"text":"hello"}`}
37			out <- provider.EventToolEnd{ID: "tool-1"}
38			out <- provider.EventDone{Stop: provider.StopToolUse, Message: provider.Message{
39				Role: provider.RoleAssistant,
40				Content: []provider.Content{provider.ToolCallBlock{
41					ID: "tool-1", Name: "echo", Arguments: json.RawMessage(`{"text":"hello"}`),
42				}},
43			}}
44			return
45		}
46		out <- provider.EventTextDelta{Delta: "tool finished"}
47		out <- provider.EventDone{Stop: provider.StopEnd, Message: provider.Message{
48			Role: provider.RoleAssistant,
49			Content: []provider.Content{
50				provider.TextBlock{Text: "tool finished"},
51			},
52		}}
53		close(c.done)
54	}()
55	return out, nil
56}
57
58type btwEchoTool struct {
59	runs atomic.Int32
60}
61
62func (t *btwEchoTool) Name() string            { return "echo" }
63func (t *btwEchoTool) Description() string     { return "echo text" }
64func (t *btwEchoTool) Schema() json.RawMessage { return json.RawMessage(`{"type":"object"}`) }
65func (t *btwEchoTool) Execute(context.Context, json.RawMessage, func(string)) (core.ToolResult, error) {
66	t.runs.Add(1)
67	return core.ToolResult{Content: []provider.Content{provider.TextBlock{Text: "hello"}}}, nil
68}
69
70func TestBtwToolExpansionUsesSideChatView(t *testing.T) {
71	result := strings.Repeat("output line\n", tui.ToolCollapseLines+10)
72	d := &btwDialog{
73		active:   true,
74		toolView: &tui.View{Theme: tui.Dark},
75		turns: []btwTurn{{
76			User: "show output",
77			Tools: []tui.ToolCallView{{
78				ID: "tool-1", Name: "bash", Result: result, Done: true,
79			}},
80		}},
81		editor: tui.NewEditor(""),
82	}
83
84	if rendered := strings.Join(d.Render(tui.Dark, 80), "\n"); !strings.Contains(rendered, "ctrl+o to expand") {
85		t.Fatalf("long /btw tool result was not initially collapsed:\n%s", rendered)
86	}
87	d.ToggleToolExpansion()
88	if rendered := strings.Join(d.Render(tui.Dark, 80), "\n"); strings.Contains(rendered, "ctrl+o to expand") {
89		t.Fatalf("long /btw tool result remained collapsed after expansion:\n%s", rendered)
90	}
91}
92
93func TestBtwCtrlOTogglesSideViewDuringConfirmation(t *testing.T) {
94	resp := make(chan core.ConfirmDecision, 1)
95	confirm := newConfirmDialog()
96	confirm.Enqueue(&confirmRequest{toolName: "bash", resp: resp})
97	btw := &btwDialog{active: true, toolView: &tui.View{}}
98	i := &Interactive{
99		view:          &tui.View{},
100		btwDialog:     btw,
101		confirmDialog: confirm,
102		dirty:         make(chan struct{}, 1),
103	}
104
105	i.handleKey(context.Background(), tui.Key{Kind: tui.KeyCtrlO})
106	if !btw.toolView.ExpandAll {
107		t.Fatal("ctrl+o did not expand /btw tool output during confirmation")
108	}
109	if i.view.ExpandAll {
110		t.Fatal("ctrl+o expanded the main view instead of the active /btw view")
111	}
112	if !confirm.Active() {
113		t.Fatal("ctrl+o closed the confirmation dialog")
114	}
115	select {
116	case decision := <-resp:
117		t.Fatalf("ctrl+o unexpectedly answered confirmation: %+v", decision)
118	default:
119	}
120}
121
122func TestBtwDialogRunsToolsWithMainAgentPolicyInIsolatedTranscript(t *testing.T) {
123	client := &btwToolTestClient{done: make(chan struct{})}
124	tool := &btwEchoTool{}
125	main := core.NewAgent(client, "main-model", "main-system", core.Registry{"echo": tool})
126	main.SetMessages([]provider.Message{{
127		Role:    provider.RoleUser,
128		Content: []provider.Content{provider.TextBlock{Text: "frozen context"}},
129	}})
130	var policyCalls atomic.Int32
131	main.BeforeToolExecute = func(provider.ToolCallBlock) (bool, string, json.RawMessage) {
132		policyCalls.Add(1)
133		return true, "", nil
134	}
135
136	d := newBtwDialog()
137	d.Open(tui.Theme{}, main, main.System, main.Model, t.TempDir(), "use the echo tool", false, false, true, nil)
138
139	select {
140	case <-client.done:
141	case <-time.After(2 * time.Second):
142		t.Fatal("timed out waiting for /btw tool turn")
143	}
144	deadline := time.Now().Add(2 * time.Second)
145	for d.Loading() && time.Now().Before(deadline) {
146		time.Sleep(time.Millisecond)
147	}
148	if d.Loading() {
149		t.Fatal("/btw remained loading after the agent turn completed")
150	}
151
152	if got := tool.runs.Load(); got != 1 {
153		t.Fatalf("tool executions = %d, want 1", got)
154	}
155	if got := policyCalls.Load(); got != 1 {
156		t.Fatalf("main agent tool-policy calls = %d, want 1", got)
157	}
158
159	client.mu.Lock()
160	requests := append([]provider.Request(nil), client.requests...)
161	client.mu.Unlock()
162	if len(requests) != 2 {
163		t.Fatalf("provider requests = %d, want 2", len(requests))
164	}
165	if len(requests[0].Tools) != 1 || requests[0].Tools[0].Name != "echo" {
166		t.Fatalf("first /btw request tools = %#v, want echo", requests[0].Tools)
167	}
168	if got := len(main.Messages()); got != 1 {
169		t.Fatalf("main transcript messages = %d, want frozen length 1", got)
170	}
171
172	d.mu.Lock()
173	if len(d.turns) != 1 {
174		t.Fatalf("side-chat turns = %d, want 1", len(d.turns))
175	}
176	turn := d.turns[0]
177	if turn.Assistant != "tool finished" {
178		t.Fatalf("assistant = %q, want %q", turn.Assistant, "tool finished")
179	}
180	if len(turn.Tools) != 1 || !turn.Tools[0].Done || turn.Tools[0].Error {
181		t.Fatalf("rendered tool state = %#v, want one successful completed tool", turn.Tools)
182	}
183	d.mu.Unlock()
184
185	rows := d.Render(tui.Theme{}, 80)
186	rendered := strings.Join(rows, "\n")
187	if !strings.Contains(rendered, "┌─") || !strings.Contains(rendered, "echo") || !strings.Contains(rendered, "└─") {
188		t.Fatalf("/btw did not use the standard bordered tool renderer:\n%s", rendered)
189	}
190	toolRow := -1
191	for idx, row := range rows {
192		if strings.Contains(row, "┌─") {
193			toolRow = idx
194			break
195		}
196	}
197	if toolRow < 1 || rows[toolRow-1] != "" {
198		t.Fatalf("/btw tool box is not separated from the user bubble by a blank row:\n%s", rendered)
199	}
200	if cursorRow, _ := d.CursorPos(80); cursorRow != len(rows)-3 {
201		t.Fatalf("cursor row = %d, want editor row %d after tool spacing change", cursorRow, len(rows)-3)
202	}
203}

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 477

↑↓ moveenter openesc close