Skip to content

Commit dc286f2

Browse files
authored
Merge pull request #1486 from krissetto/dont-confirm-on-exit-slash-cmd
When using /exit in the TUI, just exit
2 parents 7beb678 + f583cc8 commit dc286f2

File tree

2 files changed

+143
-3
lines changed

2 files changed

+143
-3
lines changed

pkg/tui/tui.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,9 +227,9 @@ func (a *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
227227
return a, cmd
228228

229229
case messages.ExitSessionMsg:
230-
return a, core.CmdHandler(dialog.OpenDialogMsg{
231-
Model: dialog.NewExitConfirmationDialog(),
232-
})
230+
// /exit command exits immediately without confirmation
231+
a.chatPage.Cleanup()
232+
return a, tea.Quit
233233

234234
case messages.NewSessionMsg:
235235
return a.handleNewSession()

pkg/tui/tui_exit_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
package tui
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
7+
"charm.land/bubbles/v2/help"
8+
"charm.land/bubbles/v2/key"
9+
tea "charm.land/bubbletea/v2"
10+
"github.com/stretchr/testify/assert"
11+
"github.com/stretchr/testify/require"
12+
13+
"github.com/docker/cagent/pkg/tui/components/completion"
14+
"github.com/docker/cagent/pkg/tui/components/notification"
15+
"github.com/docker/cagent/pkg/tui/core/layout"
16+
"github.com/docker/cagent/pkg/tui/dialog"
17+
"github.com/docker/cagent/pkg/tui/messages"
18+
)
19+
20+
// mockChatPage implements chat.Page for testing
21+
type mockChatPage struct {
22+
cleanupCalled bool
23+
}
24+
25+
func (m *mockChatPage) Init() tea.Cmd { return nil }
26+
func (m *mockChatPage) Update(tea.Msg) (layout.Model, tea.Cmd) { return m, nil }
27+
func (m *mockChatPage) View() string { return "" }
28+
func (m *mockChatPage) SetSize(int, int) tea.Cmd { return nil }
29+
func (m *mockChatPage) CompactSession(string) tea.Cmd { return nil }
30+
func (m *mockChatPage) Cleanup() { m.cleanupCalled = true }
31+
func (m *mockChatPage) GetInputHeight() int { return 0 }
32+
func (m *mockChatPage) SetSessionStarred(bool) {}
33+
func (m *mockChatPage) InsertText(string) {}
34+
func (m *mockChatPage) SetRecording(bool) tea.Cmd { return nil }
35+
func (m *mockChatPage) SendEditorContent() tea.Cmd { return nil }
36+
func (m *mockChatPage) Bindings() []key.Binding { return nil }
37+
func (m *mockChatPage) Help() help.KeyMap { return nil }
38+
39+
// collectMsgs executes a command (or batch/sequence of commands) and collects all returned messages.
40+
func collectMsgs(cmd tea.Cmd) []tea.Msg {
41+
if cmd == nil {
42+
return nil
43+
}
44+
45+
msg := cmd()
46+
if msg == nil {
47+
return nil
48+
}
49+
50+
// Handle BatchMsg
51+
if batchMsg, ok := msg.(tea.BatchMsg); ok {
52+
var msgs []tea.Msg
53+
for _, innerCmd := range batchMsg {
54+
if innerCmd != nil {
55+
msgs = append(msgs, collectMsgs(innerCmd)...)
56+
}
57+
}
58+
return msgs
59+
}
60+
61+
// Handle Sequence (unexported type, use reflection)
62+
msgValue := reflect.ValueOf(msg)
63+
if msgValue.Kind() == reflect.Slice {
64+
var msgs []tea.Msg
65+
for i := range msgValue.Len() {
66+
elem := msgValue.Index(i)
67+
if elem.CanInterface() {
68+
if innerCmd, ok := elem.Interface().(tea.Cmd); ok && innerCmd != nil {
69+
msgs = append(msgs, collectMsgs(innerCmd)...)
70+
}
71+
}
72+
}
73+
if len(msgs) > 0 {
74+
return msgs
75+
}
76+
}
77+
78+
return []tea.Msg{msg}
79+
}
80+
81+
// hasMsg checks if a message of the specified type exists in the collected messages.
82+
func hasMsg[T any](msgs []tea.Msg) bool {
83+
for _, msg := range msgs {
84+
if _, ok := msg.(T); ok {
85+
return true
86+
}
87+
}
88+
return false
89+
}
90+
91+
func TestExitSessionMsg_ExitsImmediately(t *testing.T) {
92+
t.Parallel()
93+
94+
mockPage := &mockChatPage{}
95+
96+
// Create minimal appModel with the mock chat page
97+
model := &appModel{
98+
keyMap: DefaultKeyMap(),
99+
dialog: dialog.New(),
100+
notification: notification.New(),
101+
completions: completion.New(),
102+
chatPage: mockPage,
103+
}
104+
105+
// Send ExitSessionMsg
106+
_, cmd := model.Update(messages.ExitSessionMsg{})
107+
108+
// Verify Cleanup was called
109+
assert.True(t, mockPage.cleanupCalled, "Cleanup() should be called on /exit")
110+
111+
// Verify the command produces a quit message
112+
require.NotNil(t, cmd, "cmd should not be nil")
113+
msgs := collectMsgs(cmd)
114+
assert.True(t, hasMsg[tea.QuitMsg](msgs), "should produce tea.QuitMsg for immediate exit")
115+
}
116+
117+
func TestExitConfirmedMsg_ExitsImmediately(t *testing.T) {
118+
t.Parallel()
119+
120+
mockPage := &mockChatPage{}
121+
122+
model := &appModel{
123+
keyMap: DefaultKeyMap(),
124+
dialog: dialog.New(),
125+
notification: notification.New(),
126+
completions: completion.New(),
127+
chatPage: mockPage,
128+
}
129+
130+
// Send ExitConfirmedMsg (from dialog confirmation)
131+
_, cmd := model.Update(dialog.ExitConfirmedMsg{})
132+
133+
// Verify Cleanup was called
134+
assert.True(t, mockPage.cleanupCalled, "Cleanup() should be called on exit confirmation")
135+
136+
// Verify the command produces a quit message
137+
require.NotNil(t, cmd, "cmd should not be nil")
138+
msgs := collectMsgs(cmd)
139+
assert.True(t, hasMsg[tea.QuitMsg](msgs), "should produce tea.QuitMsg")
140+
}

0 commit comments

Comments
 (0)