Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion pkg/a2a/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,6 @@ func runDockerAgent(ctx agent.InvocationContext, t *team.Team, agentName string,

case *runtime.StreamStoppedEvent:
// Send final complete event with all accumulated content

if contentBuilder.Len() > 0 {
finalEvent := &adksession.Event{
Author: agentName,
Expand Down
33 changes: 27 additions & 6 deletions pkg/tui/components/completion/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@ type QueryMsg struct {
}

type SelectedMsg struct {
Value string
Execute func() tea.Cmd
Value string
Execute func() tea.Cmd
AutoSubmit bool
}

// SelectionChangedMsg is sent when the selected item changes (for preview in editor)
Expand Down Expand Up @@ -88,6 +89,7 @@ type completionKeyMap struct {
Up key.Binding
Down key.Binding
Enter key.Binding
Tab key.Binding
Escape key.Binding
}

Expand All @@ -103,8 +105,12 @@ func defaultCompletionKeyMap() completionKeyMap {
key.WithHelp("↓", "down"),
),
Enter: key.NewBinding(
key.WithKeys("enter", "tab"),
key.WithHelp("enter/tab", "select"),
key.WithKeys("enter"),
key.WithHelp("enter", "select"),
),
Tab: key.NewBinding(
key.WithKeys("tab"),
key.WithHelp("tab", "autocomplete"),
),
Escape: key.NewBinding(
key.WithKeys("esc"),
Expand Down Expand Up @@ -255,8 +261,23 @@ func (c *manager) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
selectedItem := c.filteredItems[c.selected]
return c, tea.Sequence(
core.CmdHandler(SelectedMsg{
Value: selectedItem.Value,
Execute: selectedItem.Execute,
Value: selectedItem.Value,
Execute: selectedItem.Execute,
AutoSubmit: true,
}),
core.CmdHandler(ClosedMsg{}),
)
case key.Matches(msg, c.keyMap.Tab):
c.visible = false
if len(c.filteredItems) == 0 || c.selected >= len(c.filteredItems) {
return c, core.CmdHandler(ClosedMsg{})
}
selectedItem := c.filteredItems[c.selected]
return c, tea.Sequence(
core.CmdHandler(SelectedMsg{
Value: selectedItem.Value,
Execute: selectedItem.Execute,
AutoSubmit: false,
}),
core.CmdHandler(ClosedMsg{}),
)
Expand Down
78 changes: 78 additions & 0 deletions pkg/tui/components/completion/completion_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package completion

import (
"reflect"
"testing"

tea "charm.land/bubbletea/v2"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -335,3 +337,79 @@ func TestCompletionManagerPinnedItems(t *testing.T) {
assert.Equal(t, "main.go", m.filteredItems[1].Label, "matching item should be second")
})
}

// extractSequenceCmds extracts the slice of commands from a tea.SequenceMsg using reflection,
// since tea.sequenceMsg is unexported.
func extractSequenceCmds(c tea.Cmd) []tea.Cmd {
if c == nil {
return nil
}
seqMsg := c()
v := reflect.ValueOf(seqMsg)
var cmds []tea.Cmd
if v.Kind() == reflect.Slice {
for i := range v.Len() {
cmd, ok := v.Index(i).Interface().(tea.Cmd)
if ok {
cmds = append(cmds, cmd)
}
}
}
return cmds
}

func TestCompletionManagerAutoSubmit(t *testing.T) {
t.Parallel()

t.Run("enter triggers auto submit", func(t *testing.T) {
t.Parallel()

m := New().(*manager)

m.Update(OpenMsg{
Items: []Item{
{Label: "option", Value: "/option"},
},
})

_, c := m.Update(tea.KeyPressMsg{Code: tea.KeyEnter})

cmds := extractSequenceCmds(c)

assert.False(t, m.visible, "completion view should close")
assert.Len(t, cmds, 2, "should return a sequence of 2 commands")

if len(cmds) > 0 {
msg0 := cmds[0]()
selectedMsg, ok := msg0.(SelectedMsg)
assert.True(t, ok, "first message should be SelectedMsg")
assert.True(t, selectedMsg.AutoSubmit, "should have auto submit true")
}
})

t.Run("tab disables auto submit", func(t *testing.T) {
t.Parallel()

m := New().(*manager)

m.Update(OpenMsg{
Items: []Item{
{Label: "option", Value: "/option"},
},
})

_, c := m.Update(tea.KeyPressMsg{Code: tea.KeyTab})

cmds := extractSequenceCmds(c)

assert.False(t, m.visible, "completion view should close")
assert.Len(t, cmds, 2, "should return a sequence of 2 commands")

if len(cmds) > 0 {
msg0 := cmds[0]()
selectedMsg, ok := msg0.(SelectedMsg)
assert.True(t, ok, "first message should be SelectedMsg")
assert.False(t, selectedMsg.AutoSubmit, "should have auto submit false")
}
})
}
87 changes: 87 additions & 0 deletions pkg/tui/components/editor/completion_autosubmit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package editor

import (
"testing"

tea "charm.land/bubbletea/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/docker/docker-agent/pkg/tui/components/completion"
"github.com/docker/docker-agent/pkg/tui/messages"
)

func TestEditorHandlesAutoSubmit(t *testing.T) {
t.Parallel()

t.Run("AutoSubmit false inserts value", func(t *testing.T) {
t.Parallel()

e := newTestEditor("/he", "he")

msg := completion.SelectedMsg{
Value: "/hello",
AutoSubmit: false,
}

_, cmd := e.Update(msg)

// Command should be nil because AutoSubmit is false
assert.Nil(t, cmd)

// Value should have trigger replaced with selected value and a space appended
assert.Equal(t, "/hello ", e.textarea.Value())
})

t.Run("AutoSubmit true sends message", func(t *testing.T) {
t.Parallel()

e := newTestEditor("/he", "he")

msg := completion.SelectedMsg{
Value: "/hello",
AutoSubmit: true,
}

_, cmd := e.Update(msg)
require.NotNil(t, cmd)

// Find SendMsg
found := false
for _, m := range collectMsgs(cmd) {
if sm, ok := m.(messages.SendMsg); ok {
assert.Equal(t, "/hello", sm.Content)
found = true
break
}
}
assert.True(t, found, "should return SendMsg")
})

t.Run("AutoSubmit true with Execute runs execute command", func(t *testing.T) {
t.Parallel()

e := newTestEditor("/he", "he")

type testMsg struct{}
msg := completion.SelectedMsg{
Value: "/hello",
AutoSubmit: true,
Execute: func() tea.Cmd {
return func() tea.Msg { return testMsg{} }
},
}

_, cmd := e.Update(msg)
require.NotNil(t, cmd)

// Execute should return the provided command
msgs := collectMsgs(cmd)
require.Len(t, msgs, 1)
_, ok := msgs[0].(testMsg)
assert.True(t, ok, "should return the command from Execute")

// It should also clear the trigger and completion word from textarea
assert.Empty(t, e.textarea.Value(), "should clear the trigger and completion word")
})
}
4 changes: 0 additions & 4 deletions pkg/tui/components/editor/completions/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ func NewCommandCompletion(a *app.App) Completion {
}
}

func (c *commandCompletion) AutoSubmit() bool {
return true // Commands auto-submit: selecting inserts command text and sends it
}

func (c *commandCompletion) RequiresEmptyEditor() bool {
return true
}
Expand Down
1 change: 0 additions & 1 deletion pkg/tui/components/editor/completions/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
type Completion interface {
Trigger() string
Items() []completion.Item
AutoSubmit() bool
RequiresEmptyEditor() bool
// MatchMode returns how items should be filtered (fuzzy or prefix)
MatchMode() completion.MatchMode
Expand Down
4 changes: 2 additions & 2 deletions pkg/tui/components/editor/editor.go
Original file line number Diff line number Diff line change
Expand Up @@ -639,7 +639,7 @@ func (e *editor) Update(msg tea.Msg) (layout.Model, tea.Cmd) {

case completion.SelectedMsg:
// If the item has an Execute function, run it instead of inserting text
if msg.Execute != nil {
if msg.Execute != nil && msg.AutoSubmit {
// Remove the trigger character and any typed completion word from the textarea
// before executing. For example, typing "@" then selecting "Browse files..."
// should remove the "@" so AttachFile doesn't produce a double "@@".
Expand All @@ -654,7 +654,7 @@ func (e *editor) Update(msg tea.Msg) (layout.Model, tea.Cmd) {
e.clearSuggestion()
return e, msg.Execute()
}
if e.currentCompletion.AutoSubmit() {
if msg.AutoSubmit {
// For auto-submit completions (like commands), use the selected
// command value (e.g., "/exit") instead of what the user typed
// (e.g., "/e"). Append any extra text after the trigger word
Expand Down