From 1704df14e183e156baf552539eb07855c679d2fa Mon Sep 17 00:00:00 2001 From: Toshii <94262432+0xToshii@users.noreply.github.com> Date: Thu, 9 Oct 2025 19:23:03 -0700 Subject: [PATCH] adding config list command with renderer (#6741) * adding list command with renderer * showing empty strings for empty values --- cli/pkg/cli/config.go | 53 +++++++- cli/pkg/cli/config/manager.go | 67 +++++++++++ cli/pkg/cli/config/settings_renderer.go | 153 ++++++++++++++++++++++++ 3 files changed, 272 insertions(+), 1 deletion(-) create mode 100644 cli/pkg/cli/config/settings_renderer.go diff --git a/cli/pkg/cli/config.go b/cli/pkg/cli/config.go index 2e87b064e1..03631d1fa5 100644 --- a/cli/pkg/cli/config.go +++ b/cli/pkg/cli/config.go @@ -1,6 +1,7 @@ package cli import ( + "context" "fmt" "github.com/cline/cli/pkg/cli/config" @@ -9,6 +10,45 @@ import ( "github.com/spf13/cobra" ) +var configManager *config.Manager + +func ensureConfigManager(ctx context.Context, address string) error { + if configManager == nil || (address != "" && configManager.GetCurrentInstance() != address) { + var err error + var instanceAddress string + + if address != "" { + // Ensure instance exists at the specified address + if err := ensureInstanceAtAddress(ctx, address); err != nil { + return fmt.Errorf("failed to ensure instance at address %s: %w", address, err) + } + configManager, err = config.NewManager(ctx, address) + instanceAddress = address + } else { + // Ensure default instance exists + if err := global.EnsureDefaultInstance(ctx); err != nil { + return fmt.Errorf("failed to ensure default instance: %w", err) + } + configManager, err = config.NewManager(ctx, "") + if err == nil { + instanceAddress = configManager.GetCurrentInstance() + } + } + + if err != nil { + return fmt.Errorf("failed to create config manager: %w", err) + } + + // Always set the instance we're using as the default + registry := global.Clients.GetRegistry() + if err := registry.SetDefaultInstance(instanceAddress); err != nil { + // Log warning but don't fail - this is not critical + fmt.Printf("Warning: failed to set default instance: %v\n", err) + } + } + return nil +} + func NewConfigCommand() *cobra.Command { cmd := &cobra.Command{ Use: "config", @@ -42,16 +82,27 @@ func newConfigGetCommand() *cobra.Command { } func newConfigListCommand() *cobra.Command { + var address string + cmd := &cobra.Command{ Use: "list", Aliases: []string{"l"}, Short: "List all configuration settings", Long: `List all configuration settings from the Cline instance.`, RunE: func(cmd *cobra.Command, args []string) error { - return nil + ctx := cmd.Context() + + // Ensure config manager + if err := ensureConfigManager(ctx, address); err != nil { + return err + } + + // List settings + return configManager.ListSettings(ctx) }, } + cmd.Flags().StringVar(&address, "address", "", "specific Cline instance address to use") return cmd } diff --git a/cli/pkg/cli/config/manager.go b/cli/pkg/cli/config/manager.go index b296035204..a1b04935f2 100644 --- a/cli/pkg/cli/config/manager.go +++ b/cli/pkg/cli/config/manager.go @@ -2,6 +2,7 @@ package config import ( "context" + "encoding/json" "fmt" "github.com/cline/cli/pkg/cli/global" @@ -40,6 +41,11 @@ func NewManager(ctx context.Context, address string) (*Manager, error) { }, nil } +// GetCurrentInstance returns the address of the current instance +func (m *Manager) GetCurrentInstance() string { + return m.clientAddress +} + func (m *Manager) UpdateSettings(ctx context.Context, settings *cline.Settings, secrets *cline.Secrets) error { request := &cline.UpdateSettingsRequestCli{ Metadata: &cline.Metadata{}, @@ -57,3 +63,64 @@ func (m *Manager) UpdateSettings(ctx context.Context, settings *cline.Settings, fmt.Printf("Instance: %s\n", m.clientAddress) return nil } + +func (m *Manager) GetState(ctx context.Context) (map[string]interface{}, error) { + state, err := m.client.State.GetLatestState(ctx, &cline.EmptyRequest{}) + if err != nil { + return nil, fmt.Errorf("failed to get state: %w", err) + } + + var stateData map[string]interface{} + if err := json.Unmarshal([]byte(state.StateJson), &stateData); err != nil { + return nil, fmt.Errorf("failed to parse state: %w", err) + } + + return stateData, nil +} + +func (m *Manager) ListSettings(ctx context.Context) error { + // Get state + stateData, err := m.GetState(ctx) + if err != nil { + return err + } + + // Subset of fields we will print the values for + settingsFields := []string{ + "apiConfiguration", + "telemetrySetting", + "planActSeparateModelsSetting", + "enableCheckpointsSetting", + "mcpMarketplaceEnabled", + "shellIntegrationTimeout", + "terminalReuseEnabled", + "mcpResponsesCollapsed", + "mcpDisplayMode", + "terminalOutputLineLimit", + "mode", + "preferredLanguage", + "openaiReasoningEffort", + "strictPlanModeEnabled", + "focusChainSettings", + "useAutoCondense", + "customPrompt", + "browserSettings", + "defaultTerminalProfile", + "yoloModeToggled", + "dictationSettings", + "autoCondenseThreshold", + "autoApprovalSettings", + } + + // Render each field using the renderer + for _, field := range settingsFields { + if value, ok := stateData[field]; ok { + if err := RenderField(field, value); err != nil { + fmt.Printf("Error rendering %s: %v\n", field, err) + } + fmt.Println() + } + } + + return nil +} diff --git a/cli/pkg/cli/config/settings_renderer.go b/cli/pkg/cli/config/settings_renderer.go new file mode 100644 index 0000000000..d663f7a175 --- /dev/null +++ b/cli/pkg/cli/config/settings_renderer.go @@ -0,0 +1,153 @@ +package config + +import ( + "fmt" +) + +// formatValue formats a value for display, handling empty strings +func formatValue(val interface{}) string { + // Handle empty strings specifically + if str, ok := val.(string); ok && str == "" { + return "''" + } + return fmt.Sprintf("%v", val) +} + +// RenderField renders a single config field with proper formatting +func RenderField(key string, value interface{}) error { + switch key { + // Nested objects - render with header + nested fields + case "apiConfiguration": + return renderApiConfiguration(value) + case "browserSettings": + return renderBrowserSettings(value) + case "focusChainSettings": + return renderFocusChainSettings(value) + case "dictationSettings": + return renderDictationSettings(value) + case "autoApprovalSettings": + return renderAutoApprovalSettings(value) + + // Simple values - just print key: value + case "mode", "telemetrySetting", "preferredLanguage", "customPrompt", + "defaultTerminalProfile", "mcpDisplayMode", "openaiReasoningEffort", + "planActSeparateModelsSetting", "enableCheckpointsSetting", + "mcpMarketplaceEnabled", "terminalReuseEnabled", + "mcpResponsesCollapsed", "strictPlanModeEnabled", + "useAutoCondense", "yoloModeToggled", "shellIntegrationTimeout", + "terminalOutputLineLimit", "autoCondenseThreshold": + fmt.Printf("%s: %s\n", key, formatValue(value)) + return nil + + default: + return fmt.Errorf("unknown config field: %s", key) + } +} + +// renderApiConfiguration renders the API configuration object +func renderApiConfiguration(value interface{}) error { + fmt.Println("apiConfiguration:") + + configMap, ok := value.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid apiConfiguration format") + } + + // Print each field directly + for key, val := range configMap { + fmt.Printf(" %s: %s\n", key, formatValue(val)) + } + + return nil +} + +// renderBrowserSettings renders browser settings +func renderBrowserSettings(value interface{}) error { + fmt.Println("browserSettings:") + + settingsMap, ok := value.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid browserSettings format") + } + + // Handle nested viewport if present + if viewport, ok := settingsMap["viewport"].(map[string]interface{}); ok { + fmt.Println(" viewport:") + for key, val := range viewport { + fmt.Printf(" %s: %s\n", key, formatValue(val)) + } + } + + // Print other fields + for key, val := range settingsMap { + if key != "viewport" { + fmt.Printf(" %s: %s\n", key, formatValue(val)) + } + } + + return nil +} + +// renderFocusChainSettings renders focus chain settings +func renderFocusChainSettings(value interface{}) error { + fmt.Println("focusChainSettings:") + + settingsMap, ok := value.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid focusChainSettings format") + } + + for key, val := range settingsMap { + fmt.Printf(" %s: %s\n", key, formatValue(val)) + } + + return nil +} + +// renderDictationSettings renders dictation settings +func renderDictationSettings(value interface{}) error { + fmt.Println("dictationSettings:") + + settingsMap, ok := value.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid dictationSettings format") + } + + for key, val := range settingsMap { + fmt.Printf(" %s: %s\n", key, formatValue(val)) + } + + return nil +} + +// renderAutoApprovalSettings renders auto approval settings +func renderAutoApprovalSettings(value interface{}) error { + fmt.Println("autoApprovalSettings:") + + settingsMap, ok := value.(map[string]interface{}) + if !ok { + return fmt.Errorf("invalid autoApprovalSettings format") + } + + // Print top-level fields (skip version, handle actions specially) + for key, val := range settingsMap { + if key == "version" { + continue // Skip version + } + + if key == "actions" { + // Handle nested actions with double indentation + fmt.Println(" actions:") + if actionsMap, ok := val.(map[string]interface{}); ok { + for actionKey, actionVal := range actionsMap { + fmt.Printf(" %s: %s\n", actionKey, formatValue(actionVal)) + } + } + } else { + // Print other fields normally (enabled, maxRequests, enableNotifications, favorites) + fmt.Printf(" %s: %s\n", key, formatValue(val)) + } + } + + return nil +}