Skip to content

Commit 726d3c0

Browse files
authored
Merge pull request #661 from rumpl/command-completions
Commands auto-completion
2 parents 2dd7f9c + 8112324 commit 726d3c0

File tree

13 files changed

+349
-198
lines changed

13 files changed

+349
-198
lines changed

pkg/runtime/runtime.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1060,7 +1060,6 @@ func (r *LocalRuntime) Summarize(ctx context.Context, sess *session.Session, eve
10601060

10611061
// Check if session is empty
10621062
if len(messages) == 0 {
1063-
slog.Debug("Session is empty, nothing to summarize", "session_id", sess.ID)
10641063
events <- &WarningEvent{Message: "Session is empty. Start a conversation before compacting."}
10651064
return
10661065
}

pkg/tui/commands/commands.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
6+
tea "github.com/charmbracelet/bubbletea/v2"
7+
8+
"github.com/docker/cagent/pkg/app"
9+
"github.com/docker/cagent/pkg/tui/core"
10+
)
11+
12+
// Session commands
13+
type (
14+
NewSessionMsg struct{}
15+
EvalSessionMsg struct{}
16+
CompactSessionMsg struct{}
17+
CopySessionToClipboardMsg struct{}
18+
)
19+
20+
// Agent commands
21+
type AgentCommandMsg struct {
22+
Command string
23+
}
24+
25+
// CommandCategory represents a category of commands
26+
type Category struct {
27+
Name string
28+
Commands []Item
29+
}
30+
31+
// Command represents a single command in the palette
32+
type Item struct {
33+
ID string
34+
Label string
35+
Description string
36+
Category string
37+
SlashCommand string
38+
Execute func() tea.Cmd
39+
}
40+
41+
func BuiltInSessionCommands() []Item {
42+
return []Item{
43+
{
44+
ID: "session.new",
45+
Label: "New",
46+
SlashCommand: "/new",
47+
Description: "Start a new conversation",
48+
Category: "Session",
49+
Execute: func() tea.Cmd {
50+
return core.CmdHandler(NewSessionMsg{})
51+
},
52+
},
53+
{
54+
ID: "session.compact",
55+
Label: "Compact",
56+
SlashCommand: "/compact",
57+
Description: "Summarize the current conversation",
58+
Category: "Session",
59+
Execute: func() tea.Cmd {
60+
return core.CmdHandler(CompactSessionMsg{})
61+
},
62+
},
63+
{
64+
ID: "session.clipboard",
65+
Label: "Copy",
66+
SlashCommand: "/copy",
67+
Description: "Copy the current conversation to the clipboard",
68+
Category: "Session",
69+
Execute: func() tea.Cmd {
70+
return core.CmdHandler(CopySessionToClipboardMsg{})
71+
},
72+
},
73+
{
74+
ID: "session.eval",
75+
Label: "Eval",
76+
SlashCommand: "/eval",
77+
Description: "Create an evaluation report for the current conversation",
78+
Category: "Session",
79+
Execute: func() tea.Cmd {
80+
return core.CmdHandler(EvalSessionMsg{})
81+
},
82+
},
83+
}
84+
}
85+
86+
// BuildCommandCategories builds the list of command categories for the command palette
87+
func BuildCommandCategories(ctx context.Context, application *app.App) []Category {
88+
categories := []Category{
89+
{
90+
Name: "Session",
91+
Commands: BuiltInSessionCommands(),
92+
},
93+
}
94+
95+
agentCommands := application.CurrentAgentCommands(ctx)
96+
if len(agentCommands) == 0 {
97+
return categories
98+
}
99+
100+
commands := make([]Item, 0, len(agentCommands))
101+
for name, prompt := range agentCommands {
102+
103+
// Truncate long descriptions to fit on one line
104+
description := prompt
105+
if len(description) > 60 {
106+
description = description[:57] + "..."
107+
}
108+
109+
commands = append(commands, Item{
110+
ID: "agent.command." + name,
111+
Label: name,
112+
Description: description,
113+
Category: "Agent Commands",
114+
Execute: func() tea.Cmd {
115+
return core.CmdHandler(AgentCommandMsg{Command: "/" + name})
116+
},
117+
})
118+
}
119+
120+
categories = append(categories, Category{
121+
Name: "Agent Commands",
122+
Commands: commands,
123+
})
124+
125+
return categories
126+
}

pkg/tui/components/completion/completion.go

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ type Item struct {
2020
Label string
2121
Description string
2222
Value string
23+
Execute func() tea.Cmd
2324
}
2425

2526
type OpenMsg struct {
@@ -37,7 +38,8 @@ type QueryMsg struct {
3738
}
3839

3940
type SelectedMsg struct {
40-
Value string
41+
Value string
42+
Execute func() tea.Cmd
4143
}
4244

4345
type matchResult struct {
@@ -154,7 +156,7 @@ func (c *manager) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
154156

155157
case key.Matches(msg, c.keyMap.Enter):
156158
c.visible = false
157-
return c, core.CmdHandler(SelectedMsg{Value: c.filteredItems[c.selected].Value})
159+
return c, core.CmdHandler(SelectedMsg{Value: c.filteredItems[c.selected].Value, Execute: c.filteredItems[c.selected].Execute})
158160
case key.Matches(msg, c.keyMap.Escape):
159161
c.visible = false
160162
return c, core.CmdHandler(ClosedMsg{})
@@ -177,6 +179,14 @@ func (c *manager) View() string {
177179
visibleStart := c.scrollOffset
178180
visibleEnd := min(c.scrollOffset+maxItems, len(c.filteredItems))
179181

182+
maxLabelLen := 0
183+
for i := visibleStart; i < visibleEnd; i++ {
184+
labelLen := len(c.filteredItems[i].Label)
185+
if labelLen > maxLabelLen {
186+
maxLabelLen = labelLen
187+
}
188+
}
189+
180190
for i := visibleStart; i < visibleEnd; i++ {
181191
item := c.filteredItems[i]
182192
isSelected := i == c.selected
@@ -188,7 +198,9 @@ func (c *manager) View() string {
188198
itemStyle = styles.CompletionNormalStyle
189199
}
190200

191-
text := item.Label
201+
// Pad label to maxLabelLen so descriptions align
202+
paddedLabel := item.Label + strings.Repeat(" ", maxLabelLen+1-len(item.Label))
203+
text := paddedLabel
192204
if item.Description != "" {
193205
text += " " + styles.CompletionDescStyle.Render(item.Description)
194206
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package completions
2+
3+
import (
4+
"context"
5+
6+
"github.com/docker/cagent/pkg/app"
7+
"github.com/docker/cagent/pkg/tui/commands"
8+
"github.com/docker/cagent/pkg/tui/components/completion"
9+
)
10+
11+
type commandCompletion struct {
12+
app *app.App
13+
}
14+
15+
func NewCommandCompletion(a *app.App) Completion {
16+
return &commandCompletion{
17+
app: a,
18+
}
19+
}
20+
21+
func (c *commandCompletion) AutoSubmit() bool {
22+
return true
23+
}
24+
25+
func (c *commandCompletion) RequiresEmptyEditor() bool {
26+
return true
27+
}
28+
29+
func (c *commandCompletion) Trigger() string {
30+
return "/"
31+
}
32+
33+
func (c *commandCompletion) Items() []completion.Item {
34+
cmds := commands.BuildCommandCategories(context.Background(), c.app)
35+
items := make([]completion.Item, 0, len(cmds))
36+
for _, cmd := range cmds {
37+
for _, command := range cmd.Commands {
38+
items = append(items, completion.Item{
39+
Label: command.Label,
40+
Description: command.Description,
41+
Value: command.SlashCommand,
42+
Execute: command.Execute,
43+
})
44+
}
45+
}
46+
return items
47+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package completions
2+
3+
import (
4+
"github.com/docker/cagent/pkg/app"
5+
"github.com/docker/cagent/pkg/tui/components/completion"
6+
)
7+
8+
type Completion interface {
9+
Trigger() string
10+
Items() []completion.Item
11+
AutoSubmit() bool
12+
RequiresEmptyEditor() bool
13+
}
14+
15+
func Completions(a *app.App) []Completion {
16+
return []Completion{
17+
NewCommandCompletion(a),
18+
NewFileCompletion(),
19+
}
20+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package completions
2+
3+
import (
4+
"github.com/docker/cagent/pkg/fsx"
5+
"github.com/docker/cagent/pkg/tui/components/completion"
6+
)
7+
8+
type fileCompletion struct{}
9+
10+
func NewFileCompletion() Completion {
11+
return &fileCompletion{}
12+
}
13+
14+
func (c *fileCompletion) AutoSubmit() bool {
15+
return false
16+
}
17+
18+
func (c *fileCompletion) RequiresEmptyEditor() bool {
19+
return false
20+
}
21+
22+
func (c *fileCompletion) Trigger() string {
23+
return "@"
24+
}
25+
26+
func (c *fileCompletion) Items() []completion.Item {
27+
files, err := fsx.ListDirectory(".", 0)
28+
if err != nil {
29+
return nil
30+
}
31+
items := make([]completion.Item, len(files))
32+
for i, f := range files {
33+
items[i] = completion.Item{
34+
Label: f,
35+
Value: f,
36+
}
37+
}
38+
39+
return items
40+
}

0 commit comments

Comments
 (0)