package launch import ( "context" "errors" "fmt" "net/http" "os" "strings" "github.com/ollama/ollama/api" "github.com/ollama/ollama/cmd/config" "github.com/spf13/cobra" "golang.org/x/term" ) // LauncherState is the launch-owned snapshot used to render the root launcher menu. type LauncherState struct { LastSelection string RunModel string RunModelUsable bool Integrations map[string]LauncherIntegrationState } // LauncherIntegrationState is the launch-owned status for one launcher integration. type LauncherIntegrationState struct { Name string DisplayName string Description string Installed bool AutoInstallable bool Selectable bool Changeable bool CurrentModel string ModelUsable bool InstallHint string Editor bool } // RunModelRequest controls how the root launcher resolves the chat model. type RunModelRequest struct { ForcePicker bool Policy *LaunchPolicy } // LaunchConfirmMode controls confirmation behavior across launch flows. type LaunchConfirmMode int const ( // LaunchConfirmPrompt prompts the user for confirmation. LaunchConfirmPrompt LaunchConfirmMode = iota // LaunchConfirmAutoApprove skips prompts and treats confirmation as accepted. LaunchConfirmAutoApprove // LaunchConfirmRequireYes rejects confirmation requests with a --yes hint. LaunchConfirmRequireYes ) // LaunchMissingModelMode controls local missing-model handling in launch flows. type LaunchMissingModelMode int const ( // LaunchMissingModelPromptToPull prompts to pull a missing local model. LaunchMissingModelPromptToPull LaunchMissingModelMode = iota // LaunchMissingModelAutoPull pulls a missing local model without prompting. LaunchMissingModelAutoPull // LaunchMissingModelFail fails immediately when a local model is missing. LaunchMissingModelFail ) // LaunchPolicy controls launch behavior that may vary by caller context. type LaunchPolicy struct { Confirm LaunchConfirmMode MissingModel LaunchMissingModelMode } func defaultLaunchPolicy(interactive bool, yes bool) LaunchPolicy { policy := LaunchPolicy{ Confirm: LaunchConfirmPrompt, MissingModel: LaunchMissingModelPromptToPull, } switch { case yes: // if yes flag is set, auto approve and auto pull policy.Confirm = LaunchConfirmAutoApprove policy.MissingModel = LaunchMissingModelAutoPull case !interactive: // otherwise make sure to stop when needed policy.Confirm = LaunchConfirmRequireYes policy.MissingModel = LaunchMissingModelFail } return policy } func (p LaunchPolicy) confirmPolicy() launchConfirmPolicy { switch p.Confirm { case LaunchConfirmAutoApprove: return launchConfirmPolicy{yes: true} case LaunchConfirmRequireYes: return launchConfirmPolicy{requireYesMessage: true} default: return launchConfirmPolicy{} } } func (p LaunchPolicy) missingModelPolicy() missingModelPolicy { switch p.MissingModel { case LaunchMissingModelAutoPull: return missingModelAutoPull case LaunchMissingModelFail: return missingModelFail default: return missingModelPromptPull } } // IntegrationLaunchRequest controls the canonical integration launcher flow. type IntegrationLaunchRequest struct { Name string ModelOverride string ForceConfigure bool ConfigureOnly bool ExtraArgs []string Policy *LaunchPolicy } var isInteractiveSession = func() bool { return term.IsTerminal(int(os.Stdin.Fd())) && term.IsTerminal(int(os.Stdout.Fd())) } // Runner executes a model with an integration. type Runner interface { Run(model string, args []string) error String() string } // Editor can edit config files for integrations that support model configuration. type Editor interface { Paths() []string Edit(models []string) error Models() []string } type modelInfo struct { Name string Remote bool ToolCapable bool } // ModelInfo re-exports launcher model inventory details for callers. type ModelInfo = modelInfo // ModelItem represents a model for selection UIs. type ModelItem struct { Name string Description string Recommended bool } // LaunchCmd returns the cobra command for launching integrations. // The runTUI callback is called when the root launcher UI should be shown. func LaunchCmd(checkServerHeartbeat func(cmd *cobra.Command, args []string) error, runTUI func(cmd *cobra.Command)) *cobra.Command { var modelFlag string var configFlag bool var yesFlag 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. Flags and extra arguments require an integration name. Supported integrations: claude Claude Code cline Cline codex Codex droid Droid opencode OpenCode openclaw OpenClaw (aliases: clawdbot, moltbot) pi Pi vscode    VS Code (aliases: code) 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 { policy := defaultLaunchPolicy(isInteractiveSession(), yesFlag) // reset when done to make sure state doens't leak between launches restoreConfirmPolicy := withLaunchConfirmPolicy(policy.confirmPolicy()) defer restoreConfirmPolicy() var name string var passArgs []string dashIdx := cmd.ArgsLenAtDash() if dashIdx == -1 { 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 { 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 == "" { if cmd.Flags().Changed("model") || cmd.Flags().Changed("config") || cmd.Flags().Changed("yes") || len(passArgs) > 0 { return fmt.Errorf("flags and extra args require an integration name, for example: 'ollama launch claude --model qwen3.5'") } runTUI(cmd) return nil } if modelFlag != "" && isCloudModelName(modelFlag) { if client, err := api.ClientFromEnvironment(); err == nil { if disabled, _ := cloudStatusDisabled(cmd.Context(), client); disabled { fmt.Fprintf(os.Stderr, "Warning: ignoring --model %s because cloud is disabled\n", modelFlag) modelFlag = "" } } } headlessYes := yesFlag && !isInteractiveSession() err := LaunchIntegration(cmd.Context(), IntegrationLaunchRequest{ Name: name, ModelOverride: modelFlag, ForceConfigure: configFlag || (modelFlag == "" && !headlessYes), ConfigureOnly: configFlag, ExtraArgs: passArgs, Policy: &policy, }) if errors.Is(err, ErrCancelled) { return nil } return err }, } cmd.Flags().StringVar(&modelFlag, "model", "", "Model to use") cmd.Flags().BoolVar(&configFlag, "config", false, "Configure without launching") cmd.Flags().BoolVarP(&yesFlag, "yes", "y", false, "Automatically answer yes to confirmation prompts") return cmd } type launcherClient struct { apiClient *api.Client modelInventory []ModelInfo inventoryLoaded bool policy LaunchPolicy } func newLauncherClient(policy LaunchPolicy) (*launcherClient, error) { apiClient, err := api.ClientFromEnvironment() if err != nil { return nil, err } return &launcherClient{ apiClient: apiClient, policy: policy, }, nil } // BuildLauncherState returns the launch-owned root launcher menu snapshot. func BuildLauncherState(ctx context.Context) (*LauncherState, error) { launchClient, err := newLauncherClient(defaultLaunchPolicy(isInteractiveSession(), false)) if err != nil { return nil, err } return launchClient.buildLauncherState(ctx) } // ResolveRunModel returns the model that should be used for interactive chat. func ResolveRunModel(ctx context.Context, req RunModelRequest) (string, error) { // Called by the launcher TUI "Run a model" action (cmd/runLauncherAction), // which resolves models separately from LaunchIntegration. Callers can pass // Policy directly; otherwise we fall back to ambient --yes/session defaults. policy := defaultLaunchPolicy(isInteractiveSession(), currentLaunchConfirmPolicy.yes) if req.Policy != nil { policy = *req.Policy } launchClient, err := newLauncherClient(policy) if err != nil { return "", err } return launchClient.resolveRunModel(ctx, req) } // LaunchIntegration runs the canonical launcher flow for one integration. func LaunchIntegration(ctx context.Context, req IntegrationLaunchRequest) error { name, runner, err := LookupIntegration(req.Name) if err != nil { return err } if !req.ConfigureOnly { if err := EnsureIntegrationInstalled(name, runner); err != nil { return err } } var policy LaunchPolicy // TUI does not set a policy, whereas ollama launch does as it can have flags which change the behavior if req.Policy == nil { policy = defaultLaunchPolicy(isInteractiveSession(), false) } else { policy = *req.Policy } launchClient, err := newLauncherClient(policy) if err != nil { return err } saved, _ := loadStoredIntegrationConfig(name) // In headless --yes mode we cannot prompt, so require an explicit --model. if policy.Confirm == LaunchConfirmAutoApprove && !isInteractiveSession() && req.ModelOverride == "" { return fmt.Errorf("headless --yes launch for %s requires --model ", name) } if editor, ok := runner.(Editor); ok { return launchClient.launchEditorIntegration(ctx, name, runner, editor, saved, req) } return launchClient.launchSingleIntegration(ctx, name, runner, saved, req) } func (c *launcherClient) buildLauncherState(ctx context.Context) (*LauncherState, error) { _ = c.loadModelInventoryOnce(ctx) state := &LauncherState{ LastSelection: config.LastSelection(), RunModel: config.LastModel(), Integrations: make(map[string]LauncherIntegrationState), } runModelUsable, err := c.savedModelUsable(ctx, state.RunModel) if err != nil { runModelUsable = false } state.RunModelUsable = runModelUsable for _, info := range ListIntegrationInfos() { integrationState, err := c.buildLauncherIntegrationState(ctx, info) if err != nil { return nil, err } state.Integrations[info.Name] = integrationState } return state, nil } func (c *launcherClient) buildLauncherIntegrationState(ctx context.Context, info IntegrationInfo) (LauncherIntegrationState, error) { integration, err := integrationFor(info.Name) if err != nil { return LauncherIntegrationState{}, err } currentModel, usable, err := c.launcherModelState(ctx, info.Name, integration.editor) if err != nil { return LauncherIntegrationState{}, err } return LauncherIntegrationState{ Name: info.Name, DisplayName: info.DisplayName, Description: info.Description, Installed: integration.installed, AutoInstallable: integration.autoInstallable, Selectable: integration.installed || integration.autoInstallable, Changeable: integration.installed || integration.autoInstallable, CurrentModel: currentModel, ModelUsable: usable, InstallHint: integration.installHint, Editor: integration.editor, }, nil } func (c *launcherClient) launcherModelState(ctx context.Context, name string, isEditor bool) (string, bool, error) { cfg, loadErr := loadStoredIntegrationConfig(name) hasModels := loadErr == nil && len(cfg.Models) > 0 if !hasModels { return "", false, nil } if isEditor { filtered := c.filterDisabledCloudModels(ctx, cfg.Models) if len(filtered) > 0 { return filtered[0], true, nil } return cfg.Models[0], false, nil } model := cfg.Models[0] usable, usableErr := c.savedModelUsable(ctx, model) return model, usableErr == nil && usable, nil } func (c *launcherClient) resolveRunModel(ctx context.Context, req RunModelRequest) (string, error) { current := config.LastModel() if !req.ForcePicker && current != "" && c.policy.Confirm == LaunchConfirmAutoApprove && !isInteractiveSession() { if err := c.ensureModelsReady(ctx, []string{current}); err != nil { return "", err } fmt.Fprintf(os.Stderr, "Headless mode: auto-selected last used model %q\n", current) return current, nil } if !req.ForcePicker { usable, err := c.savedModelUsable(ctx, current) if err != nil { return "", err } if usable { if err := c.ensureModelsReady(ctx, []string{current}); err != nil { return "", err } return current, nil } } model, err := c.selectSingleModelWithSelector(ctx, "Select model to run:", current, DefaultSingleSelector) if err != nil { return "", err } if model != current { if err := config.SetLastModel(model); err != nil { return "", err } } return model, nil } func (c *launcherClient) launchSingleIntegration(ctx context.Context, name string, runner Runner, saved *config.IntegrationConfig, req IntegrationLaunchRequest) error { current := primaryModelFromConfig(saved) target := req.ModelOverride needsConfigure := req.ForceConfigure if target == "" { target = current usable, err := c.savedModelUsable(ctx, target) if err != nil { return err } if !usable { needsConfigure = true } } if needsConfigure { selected, err := c.selectSingleModelWithSelector(ctx, fmt.Sprintf("Select model for %s:", runner), target, DefaultSingleSelector) if err != nil { return err } target = selected } else if err := c.ensureModelsReady(ctx, []string{target}); err != nil { return err } if target == "" { return nil } if target != current { if err := config.SaveIntegration(name, []string{target}); err != nil { return fmt.Errorf("failed to save: %w", err) } } return launchAfterConfiguration(name, runner, target, req) } func (c *launcherClient) launchEditorIntegration(ctx context.Context, name string, runner Runner, editor Editor, saved *config.IntegrationConfig, req IntegrationLaunchRequest) error { models, needsConfigure := c.resolveEditorLaunchModels(ctx, saved, req) if needsConfigure { selected, err := c.selectMultiModelsForIntegration(ctx, runner, models) if err != nil { return err } models = selected } else if err := c.ensureModelsReady(ctx, models); err != nil { return err } if len(models) == 0 { return nil } if needsConfigure || req.ModelOverride != "" { if err := prepareEditorIntegration(name, runner, editor, models); err != nil { return err } } return launchAfterConfiguration(name, runner, models[0], req) } func (c *launcherClient) selectSingleModelWithSelector(ctx context.Context, title, current string, selector SingleSelector) (string, error) { if selector == nil { return "", fmt.Errorf("no selector configured") } items, _, err := c.loadSelectableModels(ctx, nil, current, "no models available, run 'ollama pull ' first") if err != nil { return "", err } selected, err := selector(title, items, current) if err != nil { return "", err } if err := c.ensureModelsReady(ctx, []string{selected}); err != nil { return "", err } return selected, nil } func (c *launcherClient) selectMultiModelsForIntegration(ctx context.Context, runner Runner, preChecked []string) ([]string, error) { if DefaultMultiSelector == nil { return nil, fmt.Errorf("no selector configured") } current := firstModel(preChecked) items, orderedChecked, err := c.loadSelectableModels(ctx, preChecked, current, "no models available") if err != nil { return nil, err } if len(preChecked) > 0 { // Keep list order stable in multi-select even when there are existing checks. // checked/default state still comes from orderedChecked. stableItems, _, stableErr := c.loadSelectableModels(ctx, nil, current, "no models available") if stableErr != nil { return nil, stableErr } items = stableItems } selected, err := DefaultMultiSelector(fmt.Sprintf("Select models for %s:", runner), items, orderedChecked) if err != nil { return nil, err } if err := c.ensureModelsReady(ctx, selected); err != nil { return nil, err } return selected, nil } func (c *launcherClient) loadSelectableModels(ctx context.Context, preChecked []string, current, emptyMessage string) ([]ModelItem, []string, error) { if err := c.loadModelInventoryOnce(ctx); err != nil { return nil, nil, err } cloudDisabled, _ := cloudStatusDisabled(ctx, c.apiClient) items, orderedChecked, _, _ := buildModelList(c.modelInventory, preChecked, current) if cloudDisabled { items = filterCloudItems(items) orderedChecked = c.filterDisabledCloudModels(ctx, orderedChecked) } if len(items) == 0 { return nil, nil, errors.New(emptyMessage) } return items, orderedChecked, nil } func (c *launcherClient) ensureModelsReady(ctx context.Context, models []string) error { var deduped []string seen := make(map[string]bool, len(models)) for _, model := range models { if model == "" || seen[model] { continue } seen[model] = true deduped = append(deduped, model) } models = deduped if len(models) == 0 { return nil } cloudModels := make(map[string]bool, len(models)) for _, model := range models { isCloudModel := isCloudModelName(model) if isCloudModel { cloudModels[model] = true } if err := showOrPullWithPolicy(ctx, c.apiClient, model, c.policy.missingModelPolicy(), isCloudModel); err != nil { return err } } return ensureAuth(ctx, c.apiClient, cloudModels, models) } func (c *launcherClient) resolveEditorLaunchModels(ctx context.Context, saved *config.IntegrationConfig, req IntegrationLaunchRequest) ([]string, bool) { if req.ForceConfigure { return editorPreCheckedModels(saved, req.ModelOverride), true } if req.ModelOverride != "" { models := append([]string{req.ModelOverride}, additionalSavedModels(saved, req.ModelOverride)...) models = c.filterDisabledCloudModels(ctx, models) return models, len(models) == 0 } if saved == nil || len(saved.Models) == 0 { return nil, true } models := c.filterDisabledCloudModels(ctx, saved.Models) return models, len(models) == 0 } func (c *launcherClient) filterDisabledCloudModels(ctx context.Context, models []string) []string { // if connection cannot be established or there is a 404, cloud models will continue to be displayed cloudDisabled, _ := cloudStatusDisabled(ctx, c.apiClient) if !cloudDisabled { return append([]string(nil), models...) } filtered := make([]string, 0, len(models)) for _, model := range models { if !isCloudModelName(model) { filtered = append(filtered, model) } } return filtered } func (c *launcherClient) savedModelUsable(ctx context.Context, name string) (bool, error) { if err := c.loadModelInventoryOnce(ctx); err != nil { return c.showBasedModelUsable(ctx, name) } return c.singleModelUsable(ctx, name), nil } func (c *launcherClient) showBasedModelUsable(ctx context.Context, name string) (bool, error) { if name == "" { return false, nil } info, err := c.apiClient.Show(ctx, &api.ShowRequest{Model: name}) if err != nil { var statusErr api.StatusError if errors.As(err, &statusErr) && statusErr.StatusCode == http.StatusNotFound { return false, nil } return false, err } if isCloudModelName(name) || info.RemoteModel != "" { cloudDisabled, _ := cloudStatusDisabled(ctx, c.apiClient) return !cloudDisabled, nil } return true, nil } func (c *launcherClient) singleModelUsable(ctx context.Context, name string) bool { if name == "" { return false } if isCloudModelName(name) { cloudDisabled, _ := cloudStatusDisabled(ctx, c.apiClient) return !cloudDisabled } return c.hasLocalModel(name) } func (c *launcherClient) hasLocalModel(name string) bool { for _, model := range c.modelInventory { if model.Remote { continue } if model.Name == name || strings.HasPrefix(model.Name, name+":") { return true } } return false } func (c *launcherClient) loadModelInventoryOnce(ctx context.Context) error { if c.inventoryLoaded { return nil } resp, err := c.apiClient.List(ctx) if err != nil { return err } c.modelInventory = c.modelInventory[:0] for _, model := range resp.Models { c.modelInventory = append(c.modelInventory, ModelInfo{ Name: model.Name, Remote: model.RemoteModel != "", }) } cloudDisabled, _ := cloudStatusDisabled(ctx, c.apiClient) if cloudDisabled { c.modelInventory = filterCloudModels(c.modelInventory) } c.inventoryLoaded = true return nil } func runIntegration(runner Runner, modelName string, args []string) error { fmt.Fprintf(os.Stderr, "\nLaunching %s with %s...\n", runner, modelName) return runner.Run(modelName, args) } func launchAfterConfiguration(name string, runner Runner, model string, req IntegrationLaunchRequest) error { if req.ConfigureOnly { launch, err := ConfirmPrompt(fmt.Sprintf("Launch %s now?", runner)) if err != nil { return err } if !launch { return nil } } if err := EnsureIntegrationInstalled(name, runner); err != nil { return err } return runIntegration(runner, model, req.ExtraArgs) } func loadStoredIntegrationConfig(name string) (*config.IntegrationConfig, error) { cfg, err := config.LoadIntegration(name) if err == nil { return cfg, nil } if !errors.Is(err, os.ErrNotExist) { return nil, err } spec, specErr := LookupIntegrationSpec(name) if specErr != nil { return nil, err } for _, alias := range spec.Aliases { legacy, legacyErr := config.LoadIntegration(alias) if legacyErr == nil { migrateLegacyIntegrationConfig(spec.Name, legacy) if migrated, migratedErr := config.LoadIntegration(spec.Name); migratedErr == nil { return migrated, nil } return legacy, nil } if legacyErr != nil && !errors.Is(legacyErr, os.ErrNotExist) { return nil, legacyErr } } return nil, err } func migrateLegacyIntegrationConfig(canonical string, legacy *config.IntegrationConfig) { if legacy == nil { return } _ = config.SaveIntegration(canonical, append([]string(nil), legacy.Models...)) if len(legacy.Aliases) > 0 { _ = config.SaveAliases(canonical, cloneAliases(legacy.Aliases)) } if legacy.Onboarded { _ = config.MarkIntegrationOnboarded(canonical) } } func primaryModelFromConfig(cfg *config.IntegrationConfig) string { if cfg == nil || len(cfg.Models) == 0 { return "" } return cfg.Models[0] } func cloneAliases(aliases map[string]string) map[string]string { if len(aliases) == 0 { return make(map[string]string) } cloned := make(map[string]string, len(aliases)) for key, value := range aliases { cloned[key] = value } return cloned } func firstModel(models []string) string { if len(models) == 0 { return "" } return models[0] } func editorPreCheckedModels(saved *config.IntegrationConfig, override string) []string { if override == "" { if saved == nil { return nil } return append([]string(nil), saved.Models...) } return append([]string{override}, additionalSavedModels(saved, override)...) } func additionalSavedModels(saved *config.IntegrationConfig, exclude string) []string { if saved == nil { return nil } var models []string for _, model := range saved.Models { if model != exclude { models = append(models, model) } } return models }