diff --git a/cmd/cmd.go b/cmd/cmd.go index bd0648948..2301efdfe 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -57,9 +57,9 @@ import ( func init() { // Override default selectors to use Bubbletea TUI instead of raw terminal I/O. - config.DefaultSingleSelector = func(title string, items []config.ModelItem) (string, error) { + config.DefaultSingleSelector = func(title string, items []config.ModelItem, current string) (string, error) { tuiItems := tui.ReorderItems(tui.ConvertItems(items)) - result, err := tui.SelectSingle(title, tuiItems) + result, err := tui.SelectSingle(title, tuiItems, current) if errors.Is(err, tui.ErrCancelled) { return "", config.ErrCancelled } @@ -1897,9 +1897,9 @@ func runInteractiveTUI(cmd *cobra.Command) { } // Selector adapters for tui - singleSelector := func(title string, items []config.ModelItem) (string, error) { + singleSelector := func(title string, items []config.ModelItem, current string) (string, error) { tuiItems := tui.ReorderItems(tui.ConvertItems(items)) - result, err := tui.SelectSingle(title, tuiItems) + result, err := tui.SelectSingle(title, tuiItems, current) if errors.Is(err, tui.ErrCancelled) { return "", config.ErrCancelled } diff --git a/cmd/config/claude.go b/cmd/config/claude.go index 36913d17c..b7ed02af1 100644 --- a/cmd/config/claude.go +++ b/cmd/config/claude.go @@ -126,7 +126,7 @@ func (c *Claude) ConfigureAliases(ctx context.Context, model string, existingAli fmt.Fprintf(os.Stderr, "\n%sModel Configuration%s\n\n", ansiBold, ansiReset) if aliases["primary"] == "" || force { - primary, err := DefaultSingleSelector("Select model:", items) + primary, err := DefaultSingleSelector("Select model:", items, aliases["primary"]) if err != nil { return nil, false, err } diff --git a/cmd/config/integrations.go b/cmd/config/integrations.go index 675ea70f1..1480de5f3 100644 --- a/cmd/config/integrations.go +++ b/cmd/config/integrations.go @@ -248,7 +248,8 @@ type ModelItem struct { } // SingleSelector is a function type for single item selection. -type SingleSelector func(title string, items []ModelItem) (string, error) +// current is the name of the previously selected item to highlight; empty means no pre-selection. +type SingleSelector func(title string, items []ModelItem, current string) (string, error) // MultiSelector is a function type for multi item selection. type MultiSelector func(title string, items []ModelItem, preChecked []string) ([]string, error) @@ -291,7 +292,7 @@ func SelectModelWithSelector(ctx context.Context, selector SingleSelector) (stri return "", fmt.Errorf("no models available, run 'ollama pull ' first") } - selected, err := selector("Select model to run:", items) + selected, err := selector("Select model to run:", items, "") if err != nil { return "", err } @@ -431,7 +432,7 @@ func selectIntegration() (string, error) { return strings.Compare(a.Name, b.Name) }) - return DefaultSingleSelector("Select integration:", items) + return DefaultSingleSelector("Select integration:", items, "") } // selectModelsWithSelectors lets the user select models for an integration using provided selectors. @@ -489,7 +490,7 @@ func selectModelsWithSelectors(ctx context.Context, name, current string, single if _, ok := r.(AliasConfigurer); ok { prompt = fmt.Sprintf("Select Primary model for %s:", r) } - model, err := single(prompt, items) + model, err := single(prompt, items, current) if err != nil { return nil, err } @@ -967,11 +968,9 @@ Examples: } // Validate saved model still exists - cloudCleared := false if model != "" && modelFlag == "" { if disabled, _ := cloudStatusDisabled(cmd.Context(), client); disabled && isCloudModelName(model) { model = "" - cloudCleared = true } else if _, err := client.Show(cmd.Context(), &api.ShowRequest{Model: model}); err != nil { fmt.Fprintf(os.Stderr, "%sConfigured model %q not found%s\n\n", ansiGray, model, ansiReset) if err := ShowOrPull(cmd.Context(), client, model); err != nil { @@ -980,18 +979,16 @@ Examples: } } - // If no valid model or --config flag, show picker - if model == "" || configFlag { - aliases, _, err := ac.ConfigureAliases(cmd.Context(), model, existingAliases, configFlag || cloudCleared) - if errors.Is(err, errCancelled) { - return nil - } - if err != nil { - return err - } - model = aliases["primary"] - existingAliases = aliases + // Show picker so user can change model (skip when --model flag provided) + aliases, _, err := ac.ConfigureAliases(cmd.Context(), model, existingAliases, modelFlag == "") + if errors.Is(err, errCancelled) { + return nil } + if err != nil { + return err + } + model = aliases["primary"] + existingAliases = aliases // Ensure cloud models are authenticated if isCloudModel(cmd.Context(), client, model) { @@ -1053,27 +1050,13 @@ Examples: return err } } - } else if saved, err := loadIntegration(name); err == nil && len(saved.Models) > 0 && !configFlag { - savedModels := filterDisabledCloudModels(saved.Models) - if len(savedModels) != len(saved.Models) { - _ = SaveIntegration(name, savedModels) - } - if len(savedModels) == 0 { - // All saved models were cloud — fall through to picker - models, err = selectModels(cmd.Context(), name, "") - if errors.Is(err, errCancelled) { - return nil - } - if err != nil { - return err - } - } else { - models = savedModels - return runIntegration(name, models[0], passArgs) - } } else { + current := "" + if saved, err := loadIntegration(name); err == nil && len(saved.Models) > 0 { + current = saved.Models[0] + } var err error - models, err = selectModels(cmd.Context(), name, "") + models, err = selectModels(cmd.Context(), name, current) if errors.Is(err, errCancelled) { return nil } diff --git a/cmd/tui/selector.go b/cmd/tui/selector.go index 6650f1a76..898c1396d 100644 --- a/cmd/tui/selector.go +++ b/cmd/tui/selector.go @@ -365,14 +365,27 @@ func (m selectorModel) View() string { return s } -func SelectSingle(title string, items []SelectItem) (string, error) { +// cursorForCurrent returns the item index matching current, or 0 if not found. +func cursorForCurrent(items []SelectItem, current string) int { + if current != "" { + for i, item := range items { + if item.Name == current || strings.HasPrefix(item.Name, current+":") || strings.HasPrefix(current, item.Name+":") { + return i + } + } + } + return 0 +} + +func SelectSingle(title string, items []SelectItem, current string) (string, error) { if len(items) == 0 { return "", fmt.Errorf("no items to select from") } m := selectorModel{ - title: title, - items: items, + title: title, + items: items, + cursor: cursorForCurrent(items, current), } p := tea.NewProgram(m) diff --git a/cmd/tui/selector_test.go b/cmd/tui/selector_test.go index 96ec0a90f..f87b57aac 100644 --- a/cmd/tui/selector_test.go +++ b/cmd/tui/selector_test.go @@ -382,6 +382,42 @@ func TestUpdateNavigation_Backspace(t *testing.T) { } } +// --- cursorForCurrent --- + +func TestCursorForCurrent(t *testing.T) { + testItems := []SelectItem{ + {Name: "llama3.2", Recommended: true}, + {Name: "qwen3:8b", Recommended: true}, + {Name: "gemma3:latest"}, + {Name: "deepseek-r1"}, + {Name: "glm-5:cloud"}, + } + + tests := []struct { + name string + current string + want int + }{ + {"empty current", "", 0}, + {"exact match", "qwen3:8b", 1}, + {"no match returns 0", "nonexistent", 0}, + {"bare name matches with :latest suffix", "gemma3", 2}, + {"full tag matches bare item", "llama3.2:latest", 0}, + {"cloud model exact match", "glm-5:cloud", 4}, + {"cloud model bare name", "glm-5", 4}, + {"recommended item exact match", "llama3.2", 0}, + {"recommended item with tag", "qwen3", 1}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := cursorForCurrent(testItems, tt.current); got != tt.want { + t.Errorf("cursorForCurrent(%q) = %d, want %d", tt.current, got, tt.want) + } + }) + } +} + // --- ReorderItems --- func TestReorderItems(t *testing.T) {