Skip to content

Commit 68994bd

Browse files
committed
fix: Slash Command Autocomplete vs. Auto-Execute Fix (#2257)
I have successfully implemented a root-cause fix for the issue where pressing Tab to autocomplete a slash command would immediately execute it instead of just completing the text. Root Cause Analysis: In pkg/tui/components/completion/completion.go, both Enter and Tab were bound to the same key binding, causing auto-submit completions (like slash commands) to execute immediately upon selection with either key. Solution Implemented: - Separate Tab and Enter key bindings in completion.go. - Tab now autocompletes text only (AutoExecute=false). - Enter executes the command (AutoExecute=true). - Updated editor.go to handle the AutoExecute flag. - Added comprehensive tests in pkg/tui/components/completion/autocomplete_test.go. Verification: - pkg/tui/components/completion tests: 6/6 PASS - pkg/tui/components/editor tests: PASS - Successful build of project and binary Fixes #2257
1 parent 6ecb25b commit 68994bd

File tree

3 files changed

+152
-8
lines changed

3 files changed

+152
-8
lines changed
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package completion
2+
3+
import (
4+
"testing"
5+
6+
tea "charm.land/bubbletea/v2"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestTabVsEnterBehavior(t *testing.T) {
11+
t.Run("Enter closes completion popup", func(t *testing.T) {
12+
c := New().(*manager)
13+
c.items = []Item{
14+
{Label: "exit", Description: "Exit", Value: "/exit"},
15+
}
16+
c.filterItems("")
17+
c.visible = true
18+
19+
// Press Enter
20+
result, _ := c.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
21+
22+
// Verify completion popup is closed
23+
assert.False(t, result.(*manager).visible, "Enter should close completion popup")
24+
})
25+
26+
t.Run("Tab closes completion popup", func(t *testing.T) {
27+
c := New().(*manager)
28+
c.items = []Item{
29+
{Label: "exit", Description: "Exit", Value: "/exit"},
30+
}
31+
c.filterItems("")
32+
c.visible = true
33+
34+
// Press Tab
35+
result, _ := c.Update(tea.KeyPressMsg{Code: tea.KeyTab})
36+
37+
// Verify completion popup is closed
38+
assert.False(t, result.(*manager).visible, "Tab should close completion popup")
39+
})
40+
41+
t.Run("Tab does not trigger Execute function", func(t *testing.T) {
42+
c := New().(*manager)
43+
c.items = []Item{
44+
{
45+
Label: "export",
46+
Description: "Export session",
47+
Value: "/export",
48+
Execute: func() tea.Cmd {
49+
// This should not be called for Tab
50+
t.Error("Tab should not trigger Execute function")
51+
return nil
52+
},
53+
},
54+
}
55+
c.filterItems("")
56+
c.visible = true
57+
58+
// Press Tab (should autocomplete but not execute)
59+
c.Update(tea.KeyPressMsg{Code: tea.KeyTab})
60+
61+
// If we reach here without t.Error being called, the test passes
62+
})
63+
64+
t.Run("Enter triggers Execute function", func(t *testing.T) {
65+
c := New().(*manager)
66+
c.items = []Item{
67+
{
68+
Label: "browse",
69+
Description: "Browse files",
70+
Value: "@",
71+
Execute: func() tea.Cmd {
72+
return nil
73+
},
74+
},
75+
}
76+
c.filterItems("")
77+
c.visible = true
78+
79+
// Press Enter (should execute)
80+
_, cmd := c.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
81+
82+
// The Execute function is called when the command is run by the tea runtime
83+
// For this test, we verify that a command is returned (which will call Execute when run)
84+
assert.NotNil(t, cmd, "Enter should return a command that will execute the item")
85+
})
86+
87+
t.Run("Escape closes popup without executing", func(t *testing.T) {
88+
c := New().(*manager)
89+
executed := false
90+
c.items = []Item{
91+
{
92+
Label: "exit",
93+
Description: "Exit",
94+
Value: "/exit",
95+
Execute: func() tea.Cmd {
96+
executed = true
97+
return nil
98+
},
99+
},
100+
}
101+
c.filterItems("")
102+
c.visible = true
103+
104+
// Press Escape
105+
c.Update(tea.KeyPressMsg{Code: tea.KeyEsc})
106+
107+
// Verify popup is closed and Execute was NOT called
108+
assert.False(t, c.visible, "Escape should close completion popup")
109+
assert.False(t, executed, "Escape should not trigger Execute function")
110+
})
111+
}

pkg/tui/components/completion/completion.go

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ type QueryMsg struct {
5454
type SelectedMsg struct {
5555
Value string
5656
Execute func() tea.Cmd
57+
// AutoExecute is true when Enter was pressed (should auto-submit commands)
58+
// false when Tab was pressed (just autocomplete, don't execute)
59+
AutoExecute bool
5760
}
5861

5962
// SelectionChangedMsg is sent when the selected item changes (for preview in editor)
@@ -88,6 +91,7 @@ type completionKeyMap struct {
8891
Up key.Binding
8992
Down key.Binding
9093
Enter key.Binding
94+
Tab key.Binding
9195
Escape key.Binding
9296
}
9397

@@ -103,8 +107,12 @@ func defaultCompletionKeyMap() completionKeyMap {
103107
key.WithHelp("↓", "down"),
104108
),
105109
Enter: key.NewBinding(
106-
key.WithKeys("enter", "tab"),
107-
key.WithHelp("enter/tab", "select"),
110+
key.WithKeys("enter"),
111+
key.WithHelp("enter", "select"),
112+
),
113+
Tab: key.NewBinding(
114+
key.WithKeys("tab"),
115+
key.WithHelp("tab", "autocomplete"),
108116
),
109117
Escape: key.NewBinding(
110118
key.WithKeys("esc"),
@@ -255,11 +263,28 @@ func (c *manager) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
255263
selectedItem := c.filteredItems[c.selected]
256264
return c, tea.Sequence(
257265
core.CmdHandler(SelectedMsg{
258-
Value: selectedItem.Value,
259-
Execute: selectedItem.Execute,
266+
Value: selectedItem.Value,
267+
Execute: selectedItem.Execute,
268+
AutoExecute: true, // Enter pressed - auto-execute commands
260269
}),
261270
core.CmdHandler(ClosedMsg{}),
262271
)
272+
273+
case key.Matches(msg, c.keyMap.Tab):
274+
c.visible = false
275+
if len(c.filteredItems) == 0 || c.selected >= len(c.filteredItems) {
276+
return c, core.CmdHandler(ClosedMsg{})
277+
}
278+
selectedItem := c.filteredItems[c.selected]
279+
return c, tea.Sequence(
280+
core.CmdHandler(SelectedMsg{
281+
Value: selectedItem.Value,
282+
Execute: selectedItem.Execute,
283+
AutoExecute: false, // Tab pressed - just autocomplete, don't execute
284+
}),
285+
core.CmdHandler(ClosedMsg{}),
286+
)
287+
263288
case key.Matches(msg, c.keyMap.Escape):
264289
c.visible = false
265290
return c, core.CmdHandler(ClosedMsg{})

pkg/tui/components/editor/editor.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -638,8 +638,9 @@ func (e *editor) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
638638
return e, cmd
639639

640640
case completion.SelectedMsg:
641-
// If the item has an Execute function, run it instead of inserting text
642-
if msg.Execute != nil {
641+
// If the item has an Execute function and AutoExecute is true (Enter pressed),
642+
// run it instead of inserting text
643+
if msg.Execute != nil && msg.AutoExecute {
643644
// Remove the trigger character and any typed completion word from the textarea
644645
// before executing. For example, typing "@" then selecting "Browse files..."
645646
// should remove the "@" so AttachFile doesn't produce a double "@@".
@@ -654,7 +655,10 @@ func (e *editor) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
654655
e.clearSuggestion()
655656
return e, msg.Execute()
656657
}
657-
if e.currentCompletion.AutoSubmit() {
658+
659+
// For auto-submit completions (like commands) with AutoExecute=true (Enter pressed),
660+
// use the selected command value and send it immediately
661+
if e.currentCompletion.AutoSubmit() && msg.AutoExecute {
658662
// For auto-submit completions (like commands), use the selected
659663
// command value (e.g., "/exit") instead of what the user typed
660664
// (e.g., "/e"). Append any extra text after the trigger word
@@ -667,7 +671,11 @@ func (e *editor) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
667671
cmd := e.resetAndSend(msg.Value + extraText)
668672
return e, cmd
669673
}
670-
// For non-auto-submit completions (like file paths), replace the completion word
674+
675+
// For non-auto-submit completions OR auto-submit with AutoExecute=false (Tab pressed),
676+
// just insert the text without executing
677+
// This allows Tab to autocomplete command text without executing,
678+
// so users can add arguments before pressing Enter to send
671679
currentValue := e.textarea.Value()
672680
if lastIdx := strings.LastIndex(currentValue, e.completionWord); lastIdx >= 0 {
673681
newValue := currentValue[:lastIdx-1] + msg.Value + " " + currentValue[lastIdx+len(e.completionWord):]

0 commit comments

Comments
 (0)