Skip to content
Open
Changes from all commits
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
256 changes: 142 additions & 114 deletions pkg/tui/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package commands
import (
"context"
"fmt"
"slices"
"strings"

tea "charm.land/bubbletea/v2"
Expand Down Expand Up @@ -37,69 +38,13 @@ type Item struct {
func builtInSessionCommands() []Item {
cmds := []Item{
{
ID: "session.exit",
Label: "Exit",
SlashCommand: "/exit",
Description: "Exit the application",
Category: "Session",
Execute: func(string) tea.Cmd {
return core.CmdHandler(messages.ExitSessionMsg{})
},
},
{
ID: "session.new",
Label: "New",
SlashCommand: "/new",
Description: "Start a new conversation",
Category: "Session",
Execute: func(string) tea.Cmd {
return core.CmdHandler(messages.NewSessionMsg{})
},
},
{
ID: "session.history",
Label: "Sessions",
SlashCommand: "/sessions",
Description: "Browse and load past sessions",
Category: "Session",
Execute: func(string) tea.Cmd {
return core.CmdHandler(messages.OpenSessionBrowserMsg{})
},
},
{
ID: "session.star",
Label: "Star",
SlashCommand: "/star",
Description: "Toggle star on current session",
Category: "Session",
Execute: func(string) tea.Cmd {
return core.CmdHandler(messages.ToggleSessionStarMsg{})
},
},
{
ID: "session.title",
Label: "Title",
SlashCommand: "/title",
Description: "Set or regenerate session title (usage: /title [new title])",
ID: "session.attach",
Label: "Attach",
SlashCommand: "/attach",
Description: "Attach a file to your message (usage: /attach [path])",
Category: "Session",
Execute: func(arg string) tea.Cmd {
arg = strings.TrimSpace(arg)
if arg == "" {
// No argument: regenerate title
return core.CmdHandler(messages.RegenerateTitleMsg{})
}
// With argument: set title
return core.CmdHandler(messages.SetSessionTitleMsg{Title: arg})
},
},
{
ID: "session.model",
Label: "Model",
SlashCommand: "/model",
Description: "Change the model for the current agent",
Category: "Session",
Execute: func(string) tea.Cmd {
return core.CmdHandler(messages.OpenModelPickerMsg{})
return core.CmdHandler(messages.AttachFileMsg{FilePath: arg})
},
},
{
Expand Down Expand Up @@ -132,6 +77,16 @@ func builtInSessionCommands() []Item {
return core.CmdHandler(messages.CopyLastResponseToClipboardMsg{})
},
},
{
ID: "session.cost",
Label: "Cost",
SlashCommand: "/cost",
Description: "Show detailed cost breakdown for this session",
Category: "Session",
Execute: func(string) tea.Cmd {
return core.CmdHandler(messages.ShowCostDialogMsg{})
},
},
{
ID: "session.eval",
Label: "Eval",
Expand All @@ -142,6 +97,16 @@ func builtInSessionCommands() []Item {
return core.CmdHandler(messages.EvalSessionMsg{Filename: arg})
},
},
{
ID: "session.exit",
Label: "Exit",
SlashCommand: "/exit",
Description: "Exit the application",
Category: "Session",
Execute: func(string) tea.Cmd {
return core.CmdHandler(messages.ExitSessionMsg{})
},
},
{
ID: "session.export",
Label: "Export",
Expand All @@ -153,23 +118,43 @@ func builtInSessionCommands() []Item {
},
},
{
ID: "session.yolo",
Label: "Yolo",
SlashCommand: "/yolo",
Description: "Toggle automatic approval of tool calls",
ID: "session.model",
Label: "Model",
SlashCommand: "/model",
Description: "Change the model for the current agent",
Category: "Session",
Execute: func(string) tea.Cmd {
return core.CmdHandler(messages.ToggleYoloMsg{})
return core.CmdHandler(messages.OpenModelPickerMsg{})
},
},
{
ID: "session.think",
Label: "Think",
SlashCommand: "/think",
Description: "Toggle thinking/reasoning mode",
ID: "session.new",
Label: "New",
SlashCommand: "/new",
Description: "Start a new conversation",
Category: "Session",
Execute: func(string) tea.Cmd {
return core.CmdHandler(messages.ToggleThinkingMsg{})
return core.CmdHandler(messages.NewSessionMsg{})
},
},
{
ID: "session.permissions",
Label: "Permissions",
SlashCommand: "/permissions",
Description: "Show tool permission rules for this session",
Category: "Session",
Execute: func(string) tea.Cmd {
return core.CmdHandler(messages.ShowPermissionsDialogMsg{})
},
},
{
ID: "session.history",
Label: "Sessions",
SlashCommand: "/sessions",
Description: "Browse and load past sessions",
Category: "Session",
Execute: func(string) tea.Cmd {
return core.CmdHandler(messages.OpenSessionBrowserMsg{})
},
},
{
Expand All @@ -183,45 +168,63 @@ func builtInSessionCommands() []Item {
},
},
{
ID: "session.cost",
Label: "Cost",
SlashCommand: "/cost",
Description: "Show detailed cost breakdown for this session",
ID: "session.star",
Label: "Star",
SlashCommand: "/star",
Description: "Toggle star on current session",
Category: "Session",
Execute: func(string) tea.Cmd {
return core.CmdHandler(messages.ShowCostDialogMsg{})
return core.CmdHandler(messages.ToggleSessionStarMsg{})
},
},
{
ID: "session.permissions",
Label: "Permissions",
SlashCommand: "/permissions",
Description: "Show tool permission rules for this session",
ID: "session.think",
Label: "Think",
SlashCommand: "/think",
Description: "Toggle thinking/reasoning mode",
Category: "Session",
Execute: func(string) tea.Cmd {
return core.CmdHandler(messages.ShowPermissionsDialogMsg{})
return core.CmdHandler(messages.ToggleThinkingMsg{})
},
},
{
ID: "session.attach",
Label: "Attach",
SlashCommand: "/attach",
Description: "Attach a file to your message (usage: /attach [path])",
ID: "session.title",
Label: "Title",
SlashCommand: "/title",
Description: "Set or regenerate session title (usage: /title [new title])",
Category: "Session",
Execute: func(arg string) tea.Cmd {
return core.CmdHandler(messages.AttachFileMsg{FilePath: arg})
arg = strings.TrimSpace(arg)
if arg == "" {
// No argument: regenerate title
return core.CmdHandler(messages.RegenerateTitleMsg{})
}
// With argument: set title
return core.CmdHandler(messages.SetSessionTitleMsg{Title: arg})
},
},
{
ID: "settings.theme",
Label: "Theme",
SlashCommand: "/theme",
Description: "Change the color theme",
Category: "Settings",
ID: "session.yolo",
Label: "Yolo",
SlashCommand: "/yolo",
Description: "Toggle automatic approval of tool calls",
Category: "Session",
Execute: func(string) tea.Cmd {
return core.CmdHandler(messages.OpenThemePickerMsg{})
return core.CmdHandler(messages.ToggleYoloMsg{})
},
},
}

// Add speak command on supported platforms (macOS only)
if speak := speakCommand(); speak != nil {
cmds = append(cmds, *speak)
}

return cmds
}

func builtInSettingsCommands() []Item {
return []Item{
{
ID: "settings.split-diff",
Label: "Split Diff",
Expand All @@ -232,39 +235,50 @@ func builtInSessionCommands() []Item {
return core.CmdHandler(messages.ToggleSplitDiffMsg{})
},
},
{
ID: "settings.theme",
Label: "Theme",
SlashCommand: "/theme",
Description: "Change the color theme",
Category: "Settings",
Execute: func(string) tea.Cmd {
return core.CmdHandler(messages.OpenThemePickerMsg{})
},
},
}

// Add speak command on supported platforms (macOS only)
if speak := speakCommand(); speak != nil {
cmds = append(cmds, *speak)
}

return cmds
}

func builtInFeedbackCommands() []Item {
return []Item{
{
ID: "feedback.bug",
Label: "Report Bug",
Description: "Report a bug or issue",
ID: "feedback.feedback",
Label: "Give Feedback",
Description: "Provide feedback about cagent",
Category: "Feedback",
Execute: func(string) tea.Cmd {
return core.CmdHandler(messages.OpenURLMsg{URL: "https://github.com/docker/cagent/issues/new/choose"})
return core.CmdHandler(messages.OpenURLMsg{URL: feedback.Link})
},
},
{
ID: "feedback.feedback",
Label: "Give Feedback",
Description: "Provide feedback about cagent",
ID: "feedback.bug",
Label: "Report Bug",
Description: "Report a bug or issue",
Category: "Feedback",
Execute: func(string) tea.Cmd {
return core.CmdHandler(messages.OpenURLMsg{URL: feedback.Link})
return core.CmdHandler(messages.OpenURLMsg{URL: "https://github.com/docker/cagent/issues/new/choose"})
},
},
}
}

// sortByLabel returns items sorted alphabetically by label.
func sortByLabel(items []Item) []Item {
slices.SortFunc(items, func(a, b Item) int {
return strings.Compare(strings.ToLower(a.Label), strings.ToLower(b.Label))
})
return items
}

// BuildCommandCategories builds the list of command categories for the command palette
func BuildCommandCategories(ctx context.Context, application *app.App) []Category {
// Get session commands and filter based on model capabilities
Expand Down Expand Up @@ -298,10 +312,6 @@ func BuildCommandCategories(ctx context.Context, application *app.App) []Categor
Name: "Session",
Commands: sessionCommands,
},
{
Name: "Feedback",
Commands: builtInFeedbackCommands(),
},
}

agentCommands := application.CurrentAgentCommands(ctx)
Expand All @@ -322,7 +332,7 @@ func BuildCommandCategories(ctx context.Context, application *app.App) []Categor

categories = append(categories, Category{
Name: "Agent Commands",
Commands: commands,
Commands: sortByLabel(commands),
})
}

Expand Down Expand Up @@ -394,7 +404,7 @@ func BuildCommandCategories(ctx context.Context, application *app.App) []Categor

categories = append(categories, Category{
Name: "MCP Prompts",
Commands: mcpCommands,
Commands: sortByLabel(mcpCommands),
})
}

Expand Down Expand Up @@ -424,10 +434,22 @@ func BuildCommandCategories(ctx context.Context, application *app.App) []Categor

categories = append(categories, Category{
Name: "Skills",
Commands: skillCommands,
Commands: sortByLabel(skillCommands),
})
}

// Settings and Feedback are always last, in that order.
categories = append(categories,
Category{
Name: "Settings",
Commands: builtInSettingsCommands(),
},
Category{
Name: "Feedback",
Commands: builtInFeedbackCommands(),
},
)

return categories
}

Expand All @@ -449,5 +471,11 @@ func ParseSlashCommand(input string) tea.Cmd {
}
}

for _, item := range builtInSettingsCommands() {
if item.SlashCommand == cmd {
return item.Execute(arg)
}
}

return nil
}