Skip to content
Merged
Show file tree
Hide file tree
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
29 changes: 21 additions & 8 deletions cmd/root/alias.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ func newAliasCmd() *cobra.Command {
}

type aliasAddFlags struct {
yolo bool
model string
yolo bool
model string
hideToolResults bool
}

func newAliasAddCmd() *cobra.Command {
Expand All @@ -57,8 +58,9 @@ func newAliasAddCmd() *cobra.Command {
You can optionally specify runtime options that will be applied whenever
the alias is used:

--yolo Automatically approve all tool calls without prompting
--model Override the agent's model (format: [agent=]provider/model)`,
--yolo Automatically approve all tool calls without prompting
--model Override the agent's model (format: [agent=]provider/model)
--hide-tool-results Hide tool call results in the TUI`,
Example: ` # Create a simple alias
cagent alias add code agentcatalog/notion-expert

Expand All @@ -68,7 +70,10 @@ the alias is used:
# Create an alias with a specific model
cagent alias add fast-coder agentcatalog/coder --model openai/gpt-4o-mini

# Create an alias with both options
# Create an alias with hidden tool results
cagent alias add quiet agentcatalog/coder --hide-tool-results

# Create an alias with multiple options
cagent alias add turbo agentcatalog/coder --yolo --model anthropic/claude-sonnet-4-0`,
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
Expand All @@ -78,6 +83,7 @@ the alias is used:

cmd.Flags().BoolVar(&flags.yolo, "yolo", false, "Automatically approve all tool calls without prompting")
cmd.Flags().StringVar(&flags.model, "model", "", "Override agent model (format: [agent=]provider/model)")
cmd.Flags().BoolVar(&flags.hideToolResults, "hide-tool-results", false, "Hide tool call results in the TUI")

return cmd
}
Expand Down Expand Up @@ -123,9 +129,10 @@ func runAliasAddCommand(cmd *cobra.Command, args []string, flags *aliasAddFlags)

// Create alias with options
alias := &userconfig.Alias{
Path: absAgentPath,
Yolo: flags.yolo,
Model: flags.model,
Path: absAgentPath,
Yolo: flags.yolo,
Model: flags.model,
HideToolResults: flags.hideToolResults,
}

// Store the alias
Expand All @@ -147,6 +154,9 @@ func runAliasAddCommand(cmd *cobra.Command, args []string, flags *aliasAddFlags)
if flags.model != "" {
out.Printf(" Model: %s\n", flags.model)
}
if flags.hideToolResults {
out.Printf(" Hide tool results: enabled\n")
}

if name == "default" {
out.Printf("\nYou can now run: cagent run %s (or even cagent run)\n", name)
Expand Down Expand Up @@ -201,6 +211,9 @@ func runAliasListCommand(cmd *cobra.Command, args []string) error {
if alias.Model != "" {
options = append(options, "model="+alias.Model)
}
if alias.HideToolResults {
options = append(options, "hide-tool-results")
}

if len(options) > 0 {
out.Printf(" %s%s → %s [%s]\n", name, padding, alias.Path, strings.Join(options, ", "))
Expand Down
13 changes: 12 additions & 1 deletion cmd/root/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,16 +152,27 @@ func (f *runExecFlags) runOrExec(ctx context.Context, out *cli.Printer, args []s
agentFileName = args[0]
}

// Apply global user settings first (lowest priority)
// User settings only apply if the flag wasn't explicitly set by the user
userSettings := config.GetUserSettings()
if userSettings.HideToolResults && !f.hideToolResults {
f.hideToolResults = true
slog.Debug("Applying user settings", "hide_tool_results", true)
}

// Apply alias options if this is an alias reference
// Alias options only apply if the flag wasn't explicitly set by the user
if alias := config.ResolveAlias(agentFileName); alias != nil {
slog.Debug("Applying alias options", "yolo", alias.Yolo, "model", alias.Model)
slog.Debug("Applying alias options", "yolo", alias.Yolo, "model", alias.Model, "hide_tool_results", alias.HideToolResults)
if alias.Yolo && !f.autoApprove {
f.autoApprove = true
}
if alias.Model != "" && len(f.modelOverrides) == 0 {
f.modelOverrides = append(f.modelOverrides, alias.Model)
}
if alias.HideToolResults && !f.hideToolResults {
f.hideToolResults = true
}
}

// Start fake proxy if --fake is specified
Expand Down
10 changes: 10 additions & 0 deletions pkg/config/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,16 @@ func ResolveAlias(agentFilename string) *userconfig.Alias {
return alias
}

// GetUserSettings returns the global user settings from the config file.
// Returns an empty Settings if the config file doesn't exist or has no settings.
func GetUserSettings() *userconfig.Settings {
cfg, err := userconfig.Load()
if err != nil {
return &userconfig.Settings{}
}
return cfg.GetSettings()
}

// ResolveSources resolves an agent file reference (local file, URL, or OCI image) to sources
// For OCI references, always checks remote for updates but falls back to local cache if offline
func ResolveSources(agentsPath string) (Sources, error) {
Expand Down
72 changes: 72 additions & 0 deletions pkg/config/resolve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -544,3 +544,75 @@ func TestResolveAlias_EmptyUsesDefault(t *testing.T) {
require.NotNil(t, alias)
assert.True(t, alias.Yolo)
}

func TestResolveAlias_WithHideToolResultsOption(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)

// Set up alias with hide_tool_results option
cfg, err := userconfig.Load()
require.NoError(t, err)
require.NoError(t, cfg.SetAlias("hidden-tools", &userconfig.Alias{
Path: "agentcatalog/coder",
HideToolResults: true,
}))
require.NoError(t, cfg.Save())

// Resolve alias options
alias := ResolveAlias("hidden-tools")
require.NotNil(t, alias)
assert.True(t, alias.HideToolResults)
assert.False(t, alias.Yolo)
assert.Empty(t, alias.Model)
}

func TestResolveAlias_WithAllOptions(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)

// Set up alias with all options
cfg, err := userconfig.Load()
require.NoError(t, err)
require.NoError(t, cfg.SetAlias("full", &userconfig.Alias{
Path: "agentcatalog/coder",
Yolo: true,
Model: "anthropic/claude-sonnet-4-0",
HideToolResults: true,
}))
require.NoError(t, cfg.Save())

// Resolve alias options
alias := ResolveAlias("full")
require.NotNil(t, alias)
assert.True(t, alias.Yolo)
assert.Equal(t, "anthropic/claude-sonnet-4-0", alias.Model)
assert.True(t, alias.HideToolResults)
}

func TestGetUserSettings_Empty(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)

// No config file exists
settings := GetUserSettings()
require.NotNil(t, settings)
assert.False(t, settings.HideToolResults)
}

func TestGetUserSettings_WithHideToolResults(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)

// Set up config with settings
cfg, err := userconfig.Load()
require.NoError(t, err)
cfg.Settings = &userconfig.Settings{
HideToolResults: true,
}
require.NoError(t, cfg.Save())

// Get settings
settings := GetUserSettings()
require.NotNil(t, settings)
assert.True(t, settings.HideToolResults)
}
20 changes: 19 additions & 1 deletion pkg/userconfig/userconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,19 @@ type Alias struct {
Yolo bool `yaml:"yolo,omitempty"`
// Model overrides the agent's model (format: [agent=]provider/model)
Model string `yaml:"model,omitempty"`
// HideToolResults hides tool call results in the TUI
HideToolResults bool `yaml:"hide_tool_results,omitempty"`
}

// HasOptions returns true if the alias has any runtime options set
func (a *Alias) HasOptions() bool {
return a != nil && (a.Yolo || a.Model != "")
return a != nil && (a.Yolo || a.Model != "" || a.HideToolResults)
}

// Settings represents global user settings
type Settings struct {
// HideToolResults hides tool call results in the TUI by default
HideToolResults bool `yaml:"hide_tool_results,omitempty"`
}

// CurrentVersion is the current version of the user config format
Expand All @@ -44,6 +52,8 @@ type Config struct {
ModelsGateway string `yaml:"models_gateway,omitempty"`
// Aliases maps alias names to alias configurations
Aliases map[string]*Alias `yaml:"aliases,omitempty"`
// Settings contains global user settings
Settings *Settings `yaml:"settings,omitempty"`
}

// Path returns the path to the config file
Expand Down Expand Up @@ -206,3 +216,11 @@ func (c *Config) DeleteAlias(name string) bool {
}
return false
}

// GetSettings returns the global settings, or an empty Settings if not set
func (c *Config) GetSettings() *Settings {
if c.Settings == nil {
return &Settings{}
}
return c.Settings
}
105 changes: 105 additions & 0 deletions pkg/userconfig/userconfig_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -485,3 +485,108 @@ func TestConfig_Version_LoadLegacyWithoutVersion(t *testing.T) {
require.NoError(t, config.saveTo(configFile))
assert.Equal(t, CurrentVersion, config.Version)
}

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

tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.yaml")

config := &Config{
Settings: &Settings{
HideToolResults: true,
},
}

require.NoError(t, config.saveTo(configFile))

loaded, err := loadFrom(configFile, "")
require.NoError(t, err)

assert.NotNil(t, loaded.Settings)
assert.True(t, loaded.Settings.HideToolResults)
}

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

tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.yaml")

config, err := loadFrom(configFile, "")
require.NoError(t, err)

// GetSettings should return an empty Settings struct, not nil
settings := config.GetSettings()
assert.NotNil(t, settings)
assert.False(t, settings.HideToolResults)
}

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

config := &Config{Aliases: make(map[string]*Alias)}

// GetSettings should return an empty Settings struct when Settings is nil
settings := config.GetSettings()
assert.NotNil(t, settings)
assert.False(t, settings.HideToolResults)
}

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

tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.yaml")

config := &Config{
Aliases: map[string]*Alias{
"hidden": {Path: "agentcatalog/coder", HideToolResults: true},
"full": {Path: "agentcatalog/coder", Yolo: true, Model: "openai/gpt-4o", HideToolResults: true},
},
}

require.NoError(t, config.saveTo(configFile))

loaded, err := loadFrom(configFile, "")
require.NoError(t, err)

// Verify hide_tool_results option
hiddenAlias, ok := loaded.GetAlias("hidden")
require.True(t, ok)
assert.Equal(t, "agentcatalog/coder", hiddenAlias.Path)
assert.True(t, hiddenAlias.HideToolResults)
assert.False(t, hiddenAlias.Yolo)
assert.Empty(t, hiddenAlias.Model)

// Verify all options together
fullAlias, ok := loaded.GetAlias("full")
require.True(t, ok)
assert.True(t, fullAlias.HideToolResults)
assert.True(t, fullAlias.Yolo)
assert.Equal(t, "openai/gpt-4o", fullAlias.Model)
}

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

tests := []struct {
name string
alias *Alias
expected bool
}{
{"nil alias", nil, false},
{"empty alias", &Alias{Path: "test"}, false},
{"yolo only", &Alias{Path: "test", Yolo: true}, true},
{"model only", &Alias{Path: "test", Model: "openai/gpt-4o"}, true},
{"hide_tool_results only", &Alias{Path: "test", HideToolResults: true}, true},
{"all options", &Alias{Path: "test", Yolo: true, Model: "openai/gpt-4o", HideToolResults: true}, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assert.Equal(t, tt.expected, tt.alias.HasOptions())
})
}
}