package config import ( "context" "errors" "fmt" "io" "maps" "net/http" "os" "os/exec" "path/filepath" "runtime" "slices" "strings" "time" "github.com/ollama/ollama/api" "github.com/ollama/ollama/progress" "github.com/spf13/cobra" ) // Runners execute the launching of a model with the integration - claude, codex // Editors can edit config files (supports multi-model selection) - opencode, droid // They are composable interfaces where in some cases an editor is also a runner - opencode, droid // Runner can run an integration with a model. type Runner interface { Run(model string, args []string) error // String returns the human-readable name of the integration String() string } // Editor can edit config files (supports multi-model selection) type Editor interface { // Paths returns the paths to the config files for the integration Paths() []string // Edit updates the config files for the integration with the given models Edit(models []string) error // Models returns the models currently configured for the integration // TODO(parthsareen): add error return to Models() Models() []string } // AliasConfigurer can configure model aliases (e.g., for subagent routing). // Integrations like Claude and Codex use this to route model requests to local models. type AliasConfigurer interface { // ConfigureAliases prompts the user to configure aliases and returns the updated map. ConfigureAliases(ctx context.Context, primaryModel string, existing map[string]string, force bool) (map[string]string, bool, error) // SetAliases syncs the configured aliases to the server SetAliases(ctx context.Context, aliases map[string]string) error } // integrations is the registry of available integrations. var integrations = map[string]Runner{ "claude": &Claude{}, "clawdbot": &Openclaw{}, "codex": &Codex{}, "moltbot": &Openclaw{}, "droid": &Droid{}, "opencode": &OpenCode{}, "openclaw": &Openclaw{}, "pi": &Pi{}, } // recommendedModels are shown when the user has no models or as suggestions. // Order matters: local models first, then cloud models. var recommendedModels = []ModelItem{ {Name: "glm-4.7-flash", Description: "Recommended (requires ~25GB VRAM)"}, {Name: "qwen3:8b", Description: "Recommended (requires ~11GB VRAM)"}, {Name: "glm-4.7:cloud", Description: "Recommended"}, {Name: "kimi-k2.5:cloud", Description: "Recommended"}, } // integrationAliases are hidden from the interactive selector but work as CLI arguments. var integrationAliases = map[string]bool{ "clawdbot": true, "moltbot": true, } // integrationInstallURLs maps integration names to their install script URLs. var integrationInstallURLs = map[string]string{ "claude": "https://claude.ai/install.sh", "openclaw": "https://openclaw.ai/install.sh", "droid": "https://app.factory.ai/cli", "opencode": "https://opencode.ai/install", } // CanInstallIntegration returns true if we have an install script for this integration. func CanInstallIntegration(name string) bool { _, ok := integrationInstallURLs[name] return ok } // IsIntegrationInstalled checks if an integration binary is installed. func IsIntegrationInstalled(name string) bool { switch name { case "claude": c := &Claude{} _, err := c.findPath() return err == nil case "openclaw": if _, err := exec.LookPath("openclaw"); err == nil { return true } if _, err := exec.LookPath("clawdbot"); err == nil { return true } return false case "codex": _, err := exec.LookPath("codex") return err == nil case "droid": _, err := exec.LookPath("droid") return err == nil case "opencode": _, err := exec.LookPath("opencode") return err == nil default: return true // Assume installed for unknown integrations } } // InstallIntegration downloads and runs the install script for an integration. func InstallIntegration(name string) error { url, ok := integrationInstallURLs[name] if !ok { return fmt.Errorf("no install script available for %s", name) } // Download the install script resp, err := http.Get(url) if err != nil { return fmt.Errorf("failed to download install script: %w", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("failed to download install script: HTTP %d", resp.StatusCode) } script, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read install script: %w", err) } // Create a temporary file for the script tmpDir := os.TempDir() scriptPath := filepath.Join(tmpDir, fmt.Sprintf("install-%s.sh", name)) if err := os.WriteFile(scriptPath, script, 0o700); err != nil { return fmt.Errorf("failed to write install script: %w", err) } defer os.Remove(scriptPath) // Execute the script with bash cmd := exec.Command("bash", scriptPath) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { return fmt.Errorf("install script failed: %w", err) } return nil } // SelectModel lets the user select a model to run. // ModelItem represents a model for selection. type ModelItem struct { Name string Description string } // SingleSelector is a function type for single item selection. type SingleSelector func(title string, items []ModelItem) (string, error) // MultiSelector is a function type for multi item selection. type MultiSelector func(title string, items []ModelItem, preChecked []string) ([]string, error) // SelectModelWithSelector prompts the user to select a model using the provided selector. func SelectModelWithSelector(ctx context.Context, selector SingleSelector) (string, error) { client, err := api.ClientFromEnvironment() if err != nil { return "", err } models, err := client.List(ctx) if err != nil { return "", err } var existing []modelInfo for _, m := range models.Models { existing = append(existing, modelInfo{Name: m.Name, Remote: m.RemoteModel != ""}) } lastModel := LastModel() var preChecked []string if lastModel != "" { preChecked = []string{lastModel} } items, _, existingModels, cloudModels := buildModelList(existing, preChecked, lastModel) if len(items) == 0 { return "", fmt.Errorf("no models available, run 'ollama pull ' first") } // Sort with last model first, then existing models, then recommendations slices.SortStableFunc(items, func(a, b ModelItem) int { aIsLast := a.Name == lastModel bIsLast := b.Name == lastModel if aIsLast != bIsLast { if aIsLast { return -1 } return 1 } aExists := existingModels[a.Name] bExists := existingModels[b.Name] if aExists != bExists { if aExists { return -1 } return 1 } return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name)) }) selected, err := selector("Select model to run:", items) if err != nil { return "", err } // If the selected model isn't installed, pull it first if !existingModels[selected] { msg := fmt.Sprintf("Download %s?", selected) if ok, err := confirmPrompt(msg); err != nil { return "", err } else if !ok { return "", errCancelled } fmt.Fprintf(os.Stderr, "\n") if err := pullModel(ctx, client, selected); err != nil { return "", fmt.Errorf("failed to pull %s: %w", selected, err) } } // If it's a cloud model, ensure user is signed in if cloudModels[selected] { user, err := client.Whoami(ctx) if err == nil && user != nil && user.Name != "" { return selected, nil } var aErr api.AuthorizationError if !errors.As(err, &aErr) || aErr.SigninURL == "" { return "", err } yes, err := confirmPrompt(fmt.Sprintf("sign in to use %s?", selected)) if err != nil || !yes { return "", fmt.Errorf("%s requires sign in", selected) } fmt.Fprintf(os.Stderr, "\nTo sign in, navigate to:\n %s\n\n", aErr.SigninURL) // Auto-open browser (best effort, fail silently) switch runtime.GOOS { case "darwin": _ = exec.Command("open", aErr.SigninURL).Start() case "linux": _ = exec.Command("xdg-open", aErr.SigninURL).Start() case "windows": _ = exec.Command("rundll32", "url.dll,FileProtocolHandler", aErr.SigninURL).Start() } spinnerFrames := []string{"|", "/", "-", "\\"} frame := 0 fmt.Fprintf(os.Stderr, "\033[90mwaiting for sign in to complete... %s\033[0m", spinnerFrames[0]) ticker := time.NewTicker(200 * time.Millisecond) defer ticker.Stop() for { select { case <-ctx.Done(): fmt.Fprintf(os.Stderr, "\r\033[K") return "", ctx.Err() case <-ticker.C: frame++ fmt.Fprintf(os.Stderr, "\r\033[90mwaiting for sign in to complete... %s\033[0m", spinnerFrames[frame%len(spinnerFrames)]) // poll every 10th frame (~2 seconds) if frame%10 == 0 { u, err := client.Whoami(ctx) if err == nil && u != nil && u.Name != "" { fmt.Fprintf(os.Stderr, "\r\033[K\033[A\r\033[K\033[1msigned in:\033[0m %s\n", u.Name) return selected, nil } } } } } return selected, nil } func SelectModel(ctx context.Context) (string, error) { return SelectModelWithSelector(ctx, defaultSingleSelector) } func defaultSingleSelector(title string, items []ModelItem) (string, error) { selectItems := make([]selectItem, len(items)) for i, item := range items { selectItems[i] = selectItem(item) } return selectPrompt(title, selectItems) } func defaultMultiSelector(title string, items []ModelItem, preChecked []string) ([]string, error) { selectItems := make([]selectItem, len(items)) for i, item := range items { selectItems[i] = selectItem(item) } return multiSelectPrompt(title, selectItems, preChecked) } func selectIntegration() (string, error) { if len(integrations) == 0 { return "", fmt.Errorf("no integrations available") } names := slices.Sorted(maps.Keys(integrations)) var items []selectItem for _, name := range names { if integrationAliases[name] { continue } r := integrations[name] description := r.String() if conn, err := loadIntegration(name); err == nil && len(conn.Models) > 0 { description = fmt.Sprintf("%s (%s)", r.String(), conn.Models[0]) } items = append(items, selectItem{Name: name, Description: description}) } return selectPrompt("Select integration:", items) } // selectModelsWithSelectors lets the user select models for an integration using provided selectors. func selectModelsWithSelectors(ctx context.Context, name, current string, single SingleSelector, multi MultiSelector) ([]string, error) { r, ok := integrations[name] if !ok { return nil, fmt.Errorf("unknown integration: %s", name) } client, err := api.ClientFromEnvironment() if err != nil { return nil, err } models, err := client.List(ctx) if err != nil { return nil, err } var existing []modelInfo for _, m := range models.Models { existing = append(existing, modelInfo{Name: m.Name, Remote: m.RemoteModel != ""}) } var preChecked []string if saved, err := loadIntegration(name); err == nil { preChecked = saved.Models } else if editor, ok := r.(Editor); ok { preChecked = editor.Models() } items, preChecked, existingModels, cloudModels := buildModelList(existing, preChecked, current) if len(items) == 0 { return nil, fmt.Errorf("no models available") } var selected []string if _, ok := r.(Editor); ok { selected, err = multi(fmt.Sprintf("Select models for %s:", r), items, preChecked) if err != nil { return nil, err } } else { prompt := fmt.Sprintf("Select model for %s:", r) if _, ok := r.(AliasConfigurer); ok { prompt = fmt.Sprintf("Select Primary model for %s:", r) } model, err := single(prompt, items) if err != nil { return nil, err } selected = []string{model} } var toPull []string for _, m := range selected { if !existingModels[m] { toPull = append(toPull, m) } } if len(toPull) > 0 { msg := fmt.Sprintf("Download %s?", strings.Join(toPull, ", ")) if ok, err := confirmPrompt(msg); err != nil { return nil, err } else if !ok { return nil, errCancelled } for _, m := range toPull { fmt.Fprintf(os.Stderr, "\n") if err := pullModel(ctx, client, m); err != nil { return nil, fmt.Errorf("failed to pull %s: %w", m, err) } } } if err := ensureAuth(ctx, client, cloudModels, selected); err != nil { return nil, err } return selected, nil } func pullIfNeeded(ctx context.Context, client *api.Client, existingModels map[string]bool, model string) error { if existingModels[model] { return nil } msg := fmt.Sprintf("Download %s?", model) if ok, err := confirmPrompt(msg); err != nil { return err } else if !ok { return errCancelled } fmt.Fprintf(os.Stderr, "\n") if err := pullModel(ctx, client, model); err != nil { return fmt.Errorf("failed to pull %s: %w", model, err) } return nil } // showOrPull checks if a model exists via client.Show and offers to pull it if not found. func showOrPull(ctx context.Context, client *api.Client, model string) error { if _, err := client.Show(ctx, &api.ShowRequest{Model: model}); err == nil { return nil } if ok, err := confirmPrompt(fmt.Sprintf("Download %s?", model)); err != nil { return err } else if !ok { return errCancelled } fmt.Fprintf(os.Stderr, "\n") return pullModel(ctx, client, model) } func listModels(ctx context.Context) ([]selectItem, map[string]bool, map[string]bool, *api.Client, error) { client, err := api.ClientFromEnvironment() if err != nil { return nil, nil, nil, nil, err } models, err := client.List(ctx) if err != nil { return nil, nil, nil, nil, err } var existing []modelInfo for _, m := range models.Models { existing = append(existing, modelInfo{ Name: m.Name, Remote: m.RemoteModel != "", }) } modelItems, _, existingModels, cloudModels := buildModelList(existing, nil, "") if len(modelItems) == 0 { return nil, nil, nil, nil, fmt.Errorf("no models available, run 'ollama pull ' first") } items := make([]selectItem, len(modelItems)) for i, mi := range modelItems { items[i] = selectItem(mi) } return items, existingModels, cloudModels, client, nil } func ensureAuth(ctx context.Context, client *api.Client, cloudModels map[string]bool, selected []string) error { var selectedCloudModels []string for _, m := range selected { if cloudModels[m] { selectedCloudModels = append(selectedCloudModels, m) } } if len(selectedCloudModels) == 0 { return nil } user, err := client.Whoami(ctx) if err == nil && user != nil && user.Name != "" { return nil } var aErr api.AuthorizationError if !errors.As(err, &aErr) || aErr.SigninURL == "" { return err } modelList := strings.Join(selectedCloudModels, ", ") yes, err := confirmPrompt(fmt.Sprintf("sign in to use %s?", modelList)) if err != nil || !yes { return fmt.Errorf("%s requires sign in", modelList) } fmt.Fprintf(os.Stderr, "\nTo sign in, navigate to:\n %s\n\n", aErr.SigninURL) switch runtime.GOOS { case "darwin": _ = exec.Command("open", aErr.SigninURL).Start() case "linux": _ = exec.Command("xdg-open", aErr.SigninURL).Start() case "windows": _ = exec.Command("rundll32", "url.dll,FileProtocolHandler", aErr.SigninURL).Start() } spinnerFrames := []string{"|", "/", "-", "\\"} frame := 0 fmt.Fprintf(os.Stderr, "\033[90mwaiting for sign in to complete... %s\033[0m", spinnerFrames[0]) ticker := time.NewTicker(200 * time.Millisecond) defer ticker.Stop() for { select { case <-ctx.Done(): fmt.Fprintf(os.Stderr, "\r\033[K") return ctx.Err() case <-ticker.C: frame++ fmt.Fprintf(os.Stderr, "\r\033[90mwaiting for sign in to complete... %s\033[0m", spinnerFrames[frame%len(spinnerFrames)]) // poll every 10th frame (~2 seconds) if frame%10 == 0 { u, err := client.Whoami(ctx) if err == nil && u != nil && u.Name != "" { fmt.Fprintf(os.Stderr, "\r\033[K\033[A\r\033[K\033[1msigned in:\033[0m %s\n", u.Name) return nil } } } } } // selectModels lets the user select models for an integration using default selectors. func selectModels(ctx context.Context, name, current string) ([]string, error) { return selectModelsWithSelectors(ctx, name, current, defaultSingleSelector, defaultMultiSelector) } func runIntegration(name, modelName string, args []string) error { r, ok := integrations[name] if !ok { return fmt.Errorf("unknown integration: %s", name) } fmt.Fprintf(os.Stderr, "\nLaunching %s with %s...\n", r, modelName) return r.Run(modelName, args) } // syncAliases syncs aliases to server and saves locally for an AliasConfigurer. func syncAliases(ctx context.Context, client *api.Client, ac AliasConfigurer, name, model string, existing map[string]string) error { aliases := make(map[string]string) for k, v := range existing { aliases[k] = v } aliases["primary"] = model if isCloudModel(ctx, client, model) { if aliases["fast"] == "" || !isCloudModel(ctx, client, aliases["fast"]) { aliases["fast"] = model } } else { delete(aliases, "fast") } if err := ac.SetAliases(ctx, aliases); err != nil { return err } return saveAliases(name, aliases) } // LaunchIntegration launches the named integration using saved config or prompts for setup. func LaunchIntegration(name string) error { r, ok := integrations[name] if !ok { return fmt.Errorf("unknown integration: %s", name) } // Try to use saved config if config, err := loadIntegration(name); err == nil && len(config.Models) > 0 { return runIntegration(name, config.Models[0], nil) } // No saved config - prompt user to run setup return fmt.Errorf("%s is not configured. Run 'ollama launch %s' to set it up", r, name) } // LaunchIntegrationWithModel launches the named integration with the specified model. func LaunchIntegrationWithModel(name, modelName string) error { return runIntegration(name, modelName, nil) } // SaveIntegrationModel saves the model for an integration. func SaveIntegrationModel(name, modelName string) error { // Load existing models and prepend the new one var models []string if existing, err := loadIntegration(name); err == nil && len(existing.Models) > 0 { models = existing.Models // Remove the model if it already exists for i, m := range models { if m == modelName { models = append(models[:i], models[i+1:]...) break } } } // Prepend the new model models = append([]string{modelName}, models...) return saveIntegration(name, models) } // ConfigureIntegrationWithSelectors allows the user to select/change the model for an integration using custom selectors. func ConfigureIntegrationWithSelectors(ctx context.Context, name string, single SingleSelector, multi MultiSelector) error { r, ok := integrations[name] if !ok { return fmt.Errorf("unknown integration: %s", name) } models, err := selectModelsWithSelectors(ctx, name, "", single, multi) if errors.Is(err, errCancelled) { return nil } if err != nil { return err } if editor, isEditor := r.(Editor); isEditor { paths := editor.Paths() if len(paths) > 0 { fmt.Fprintf(os.Stderr, "This will modify your %s configuration:\n", r) for _, p := range paths { fmt.Fprintf(os.Stderr, " %s\n", p) } fmt.Fprintf(os.Stderr, "Backups will be saved to %s/\n\n", backupDir()) if ok, _ := confirmPrompt("Proceed?"); !ok { return nil } } if err := editor.Edit(models); err != nil { return fmt.Errorf("setup failed: %w", err) } } if err := saveIntegration(name, models); err != nil { return fmt.Errorf("failed to save: %w", err) } if len(models) == 1 { fmt.Fprintf(os.Stderr, "Configured %s with %s\n", r, models[0]) } else { fmt.Fprintf(os.Stderr, "Configured %s with %d models (default: %s)\n", r, len(models), models[0]) } return nil } // ConfigureIntegration allows the user to select/change the model for an integration. func ConfigureIntegration(ctx context.Context, name string) error { return ConfigureIntegrationWithSelectors(ctx, name, defaultSingleSelector, defaultMultiSelector) } // LaunchCmd returns the cobra command for launching integrations. // The runTUI callback is called when no arguments are provided (alias for main TUI). func LaunchCmd(checkServerHeartbeat func(cmd *cobra.Command, args []string) error, runTUI func(cmd *cobra.Command)) *cobra.Command { var modelFlag string var configFlag bool cmd := &cobra.Command{ Use: "launch [INTEGRATION] [-- [EXTRA_ARGS...]]", Short: "Launch the Ollama menu or an integration", Long: `Launch the Ollama interactive menu, or directly launch a specific integration. Without arguments, this is equivalent to running 'ollama' directly. Supported integrations: claude Claude Code codex Codex droid Droid opencode OpenCode openclaw OpenClaw (aliases: clawdbot, moltbot) Examples: ollama launch ollama launch claude ollama launch claude --model ollama launch droid --config (does not auto-launch) ollama launch codex -- -p myprofile (pass extra args to integration) ollama launch codex -- --sandbox workspace-write`, Args: cobra.ArbitraryArgs, PreRunE: checkServerHeartbeat, RunE: func(cmd *cobra.Command, args []string) error { // No args - run the main TUI (same as 'ollama') if len(args) == 0 && modelFlag == "" && !configFlag { runTUI(cmd) return nil } // Extract integration name and args to pass through using -- separator var name string var passArgs []string dashIdx := cmd.ArgsLenAtDash() if dashIdx == -1 { // No "--" separator: only allow 0 or 1 args (integration name) if len(args) > 1 { return fmt.Errorf("unexpected arguments: %v\nUse '--' to pass extra arguments to the integration", args[1:]) } if len(args) == 1 { name = args[0] } } else { // "--" was used: args before it = integration name, args after = passthrough if dashIdx > 1 { return fmt.Errorf("expected at most 1 integration name before '--', got %d", dashIdx) } if dashIdx == 1 { name = args[0] } passArgs = args[dashIdx:] } if name == "" { var err error name, err = selectIntegration() if errors.Is(err, errCancelled) { return nil } if err != nil { return err } } r, ok := integrations[strings.ToLower(name)] if !ok { return fmt.Errorf("unknown integration: %s", name) } // Handle AliasConfigurer integrations (claude, codex) if ac, ok := r.(AliasConfigurer); ok { client, err := api.ClientFromEnvironment() if err != nil { return err } // Validate --model flag if provided if modelFlag != "" { if err := showOrPull(cmd.Context(), client, modelFlag); err != nil { if errors.Is(err, errCancelled) { return nil } return err } } var model string var existingAliases map[string]string // Load saved config if cfg, err := loadIntegration(name); err == nil { existingAliases = cfg.Aliases if len(cfg.Models) > 0 { model = cfg.Models[0] // AliasConfigurer integrations use single model; sanitize if multiple if len(cfg.Models) > 1 { _ = saveIntegration(name, []string{model}) } } } // --model flag overrides saved model if modelFlag != "" { model = modelFlag } // Validate saved model still exists if model != "" && modelFlag == "" { 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 { model = "" } } } // If no valid model or --config flag, show picker if model == "" || configFlag { aliases, _, err := ac.ConfigureAliases(cmd.Context(), model, existingAliases, configFlag) 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) { if err := ensureAuth(cmd.Context(), client, map[string]bool{model: true}, []string{model}); err != nil { return err } } // Sync aliases and save if err := syncAliases(cmd.Context(), client, ac, name, model, existingAliases); err != nil { fmt.Fprintf(os.Stderr, "%sWarning: Could not sync aliases: %v%s\n", ansiGray, err, ansiReset) } if err := saveIntegration(name, []string{model}); err != nil { return fmt.Errorf("failed to save: %w", err) } // Launch (unless --config without confirmation) if configFlag { if launch, _ := confirmPrompt(fmt.Sprintf("Launch %s now?", r)); launch { return runIntegration(name, model, passArgs) } return nil } return runIntegration(name, model, passArgs) } // Validate --model flag for non-AliasConfigurer integrations if modelFlag != "" { client, err := api.ClientFromEnvironment() if err != nil { return err } if err := showOrPull(cmd.Context(), client, modelFlag); err != nil { if errors.Is(err, errCancelled) { return nil } return err } } var models []string if modelFlag != "" { models = []string{modelFlag} if existing, err := loadIntegration(name); err == nil && len(existing.Models) > 0 { for _, m := range existing.Models { if m != modelFlag { models = append(models, m) } } } } else if saved, err := loadIntegration(name); err == nil && len(saved.Models) > 0 && !configFlag { return runIntegration(name, saved.Models[0], passArgs) } else { var err error models, err = selectModels(cmd.Context(), name, "") if errors.Is(err, errCancelled) { return nil } if err != nil { return err } } if editor, isEditor := r.(Editor); isEditor { paths := editor.Paths() if len(paths) > 0 { fmt.Fprintf(os.Stderr, "This will modify your %s configuration:\n", r) for _, p := range paths { fmt.Fprintf(os.Stderr, " %s\n", p) } fmt.Fprintf(os.Stderr, "Backups will be saved to %s/\n\n", backupDir()) if ok, _ := confirmPrompt("Proceed?"); !ok { return nil } } } if err := saveIntegration(name, models); err != nil { return fmt.Errorf("failed to save: %w", err) } if editor, isEditor := r.(Editor); isEditor { if err := editor.Edit(models); err != nil { return fmt.Errorf("setup failed: %w", err) } } if _, isEditor := r.(Editor); isEditor { if len(models) == 1 { fmt.Fprintf(os.Stderr, "Added %s to %s\n", models[0], r) } else { fmt.Fprintf(os.Stderr, "Added %d models to %s (default: %s)\n", len(models), r, models[0]) } } if configFlag { if launch, _ := confirmPrompt(fmt.Sprintf("\nLaunch %s now?", r)); launch { return runIntegration(name, models[0], passArgs) } fmt.Fprintf(os.Stderr, "Run 'ollama launch %s' to start with %s\n", strings.ToLower(name), models[0]) return nil } return runIntegration(name, models[0], passArgs) }, } cmd.Flags().StringVar(&modelFlag, "model", "", "Model to use") cmd.Flags().BoolVar(&configFlag, "config", false, "Configure without launching") return cmd } type modelInfo struct { Name string Remote bool ToolCapable bool } // buildModelList merges existing models with recommendations, sorts them, and returns // the ordered items along with maps of existing and cloud model names. func buildModelList(existing []modelInfo, preChecked []string, current string) (items []ModelItem, orderedChecked []string, existingModels, cloudModels map[string]bool) { existingModels = make(map[string]bool) cloudModels = make(map[string]bool) recommended := make(map[string]bool) var hasLocalModel, hasCloudModel bool for _, rec := range recommendedModels { recommended[rec.Name] = true } for _, m := range existing { existingModels[m.Name] = true if m.Remote { cloudModels[m.Name] = true hasCloudModel = true } else { hasLocalModel = true } displayName := strings.TrimSuffix(m.Name, ":latest") existingModels[displayName] = true item := ModelItem{Name: displayName} if recommended[displayName] { item.Description = "recommended" } items = append(items, item) } for _, rec := range recommendedModels { if existingModels[rec.Name] || existingModels[rec.Name+":latest"] { continue } items = append(items, rec) if strings.HasSuffix(rec.Name, ":cloud") { cloudModels[rec.Name] = true } } checked := make(map[string]bool, len(preChecked)) for _, n := range preChecked { checked[n] = true } // Resolve current to full name (e.g., "llama3.2" -> "llama3.2:latest") for _, item := range items { if item.Name == current || strings.HasPrefix(item.Name, current+":") { current = item.Name break } } if checked[current] { preChecked = append([]string{current}, slices.DeleteFunc(preChecked, func(m string) bool { return m == current })...) } // Non-existing models get "install?" suffix and are pushed to the bottom. // When user has no models, preserve recommended order. notInstalled := make(map[string]bool) for i := range items { if !existingModels[items[i].Name] { notInstalled[items[i].Name] = true if items[i].Description != "" { items[i].Description += ", install?" } else { items[i].Description = "install?" } } } if hasLocalModel || hasCloudModel { slices.SortStableFunc(items, func(a, b ModelItem) int { ac, bc := checked[a.Name], checked[b.Name] aNew, bNew := notInstalled[a.Name], notInstalled[b.Name] if ac != bc { if ac { return -1 } return 1 } if !ac && !bc && aNew != bNew { if aNew { return 1 } return -1 } return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name)) }) } return items, preChecked, existingModels, cloudModels } // isCloudModel checks if a model is a cloud model using the Show API. func isCloudModel(ctx context.Context, client *api.Client, name string) bool { if client == nil { return false } resp, err := client.Show(ctx, &api.ShowRequest{Model: name}) if err != nil { return false } return resp.RemoteModel != "" } // GetModelItems returns a list of model items including recommendations for the TUI. // It includes all locally available models plus recommended models that aren't installed. func GetModelItems(ctx context.Context) ([]ModelItem, map[string]bool) { client, err := api.ClientFromEnvironment() if err != nil { return nil, nil } models, err := client.List(ctx) if err != nil { return nil, nil } var existing []modelInfo for _, m := range models.Models { existing = append(existing, modelInfo{Name: m.Name, Remote: m.RemoteModel != ""}) } lastModel := LastModel() var preChecked []string if lastModel != "" { preChecked = []string{lastModel} } items, _, existingModels, _ := buildModelList(existing, preChecked, lastModel) // Sort with last model first, then existing models, then recommendations slices.SortStableFunc(items, func(a, b ModelItem) int { aIsLast := a.Name == lastModel bIsLast := b.Name == lastModel if aIsLast != bIsLast { if aIsLast { return -1 } return 1 } aExists := existingModels[a.Name] bExists := existingModels[b.Name] if aExists != bExists { if aExists { return -1 } return 1 } return strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name)) }) return items, existingModels } func pullModel(ctx context.Context, client *api.Client, model string) error { p := progress.NewProgress(os.Stderr) defer p.Stop() bars := make(map[string]*progress.Bar) var status string var spinner *progress.Spinner fn := func(resp api.ProgressResponse) error { if resp.Digest != "" { if resp.Completed == 0 { return nil } if spinner != nil { spinner.Stop() } bar, ok := bars[resp.Digest] if !ok { name, isDigest := strings.CutPrefix(resp.Digest, "sha256:") name = strings.TrimSpace(name) if isDigest { name = name[:min(12, len(name))] } bar = progress.NewBar(fmt.Sprintf("pulling %s:", name), resp.Total, resp.Completed) bars[resp.Digest] = bar p.Add(resp.Digest, bar) } bar.Set(resp.Completed) } else if status != resp.Status { if spinner != nil { spinner.Stop() } status = resp.Status spinner = progress.NewSpinner(status) p.Add(status, spinner) } return nil } request := api.PullRequest{Name: model} return client.Pull(ctx, &request, fn) }