Compare commits

..

4 Commits

Author SHA1 Message Date
Bruce MacDonald
3ecb89007e pr feedback 2026-02-11 13:46:37 -08:00
Bruce MacDonald
da0190f8e4 Update quickstart.mdx 2026-02-11 12:16:31 -08:00
Bruce MacDonald
1f2594f50f Update quickstart.mdx 2026-02-11 12:15:02 -08:00
Bruce MacDonald
3fb0958e01 docs: update quickstart for tui 2026-02-11 12:08:55 -08:00
17 changed files with 147 additions and 860 deletions

View File

@@ -58,7 +58,10 @@ 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) {
tuiItems := tui.ReorderItems(tui.ConvertItems(items))
tuiItems := make([]tui.SelectItem, len(items))
for i, item := range items {
tuiItems[i] = tui.SelectItem{Name: item.Name, Description: item.Description, Recommended: item.Recommended}
}
result, err := tui.SelectSingle(title, tuiItems)
if errors.Is(err, tui.ErrCancelled) {
return "", config.ErrCancelled
@@ -67,7 +70,10 @@ func init() {
}
config.DefaultMultiSelector = func(title string, items []config.ModelItem, preChecked []string) ([]string, error) {
tuiItems := tui.ReorderItems(tui.ConvertItems(items))
tuiItems := make([]tui.SelectItem, len(items))
for i, item := range items {
tuiItems[i] = tui.SelectItem{Name: item.Name, Description: item.Description, Recommended: item.Recommended}
}
result, err := tui.SelectMultiple(title, tuiItems, preChecked)
if errors.Is(err, tui.ErrCancelled) {
return nil, config.ErrCancelled
@@ -2007,17 +2013,9 @@ func runInteractiveTUI(cmd *cobra.Command) {
}
case tui.SelectionChangeIntegration:
_ = config.SetLastSelection(result.Integration)
if len(result.Models) > 0 {
// Multi-select from modal (Editor integrations)
if err := config.SaveAndEditIntegration(result.Integration, result.Models); err != nil {
fmt.Fprintf(os.Stderr, "Error configuring %s: %v\n", result.Integration, err)
continue
}
if err := config.LaunchIntegrationWithModel(result.Integration, result.Models[0]); err != nil {
fmt.Fprintf(os.Stderr, "Error launching %s: %v\n", result.Integration, err)
}
} else if result.Model != "" {
// Single-select from modal - save and launch
// Use model from modal if selected, otherwise show picker
if result.Model != "" {
// Model already selected from modal - save and launch
if err := config.SaveIntegrationModel(result.Integration, result.Model); err != nil {
fmt.Fprintf(os.Stderr, "Error saving config: %v\n", err)
continue
@@ -2262,7 +2260,7 @@ func NewCLI() *cobra.Command {
switch cmd {
case runCmd:
imagegen.AppendFlagsDocs(cmd)
appendEnvDocs(cmd, []envconfig.EnvVar{envVars["OLLAMA_EDITOR"], envVars["OLLAMA_HOST"], envVars["OLLAMA_NOHISTORY"]})
appendEnvDocs(cmd, []envconfig.EnvVar{envVars["OLLAMA_HOST"], envVars["OLLAMA_NOHISTORY"]})
case serveCmd:
appendEnvDocs(cmd, []envconfig.EnvVar{
envVars["OLLAMA_DEBUG"],

View File

@@ -160,15 +160,6 @@ func IntegrationModel(appName string) string {
return ic.Models[0]
}
// IntegrationModels returns all configured models for an integration, or nil.
func IntegrationModels(appName string) []string {
ic, err := loadIntegration(appName)
if err != nil || len(ic.Models) == 0 {
return nil
}
return ic.Models
}
// LastModel returns the last model that was run, or empty string if none.
func LastModel() string {
cfg, err := load()

View File

@@ -63,8 +63,8 @@ var integrations = map[string]Runner{
// 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-5:cloud", Description: "Reasoning and code generation", Recommended: true},
{Name: "kimi-k2.5:cloud", Description: "Multimodal reasoning with subagents", Recommended: true},
{Name: "glm-4.7:cloud", Description: "Reasoning and code generation", Recommended: true},
{Name: "glm-4.7-flash", Description: "Reasoning and code generation locally", Recommended: true},
{Name: "qwen3:8b", Description: "Efficient all-purpose assistant", Recommended: true},
}
@@ -171,17 +171,6 @@ func IsIntegrationInstalled(name string) bool {
}
}
// IsEditorIntegration returns true if the named integration uses multi-model
// selection (implements the Editor interface).
func IsEditorIntegration(name string) bool {
r, ok := integrations[strings.ToLower(name)]
if !ok {
return false
}
_, isEditor := r.(Editor)
return isEditor
}
// SelectModel lets the user select a model to run.
// ModelItem represents a model for selection.
type ModelItem struct {
@@ -232,22 +221,15 @@ func SelectModelWithSelector(ctx context.Context, selector SingleSelector) (stri
// If the selected model isn't installed, pull it first
if !existingModels[selected] {
if cloudModels[selected] {
// Cloud models only pull a small manifest; no confirmation needed
if err := pullModel(ctx, client, selected); err != nil {
return "", fmt.Errorf("failed to pull %s: %w", selected, err)
}
} else {
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)
}
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)
}
}
@@ -456,11 +438,6 @@ func ShowOrPull(ctx context.Context, client *api.Client, model string) error {
if _, err := client.Show(ctx, &api.ShowRequest{Model: model}); err == nil {
return nil
}
// Cloud models only pull a small manifest; skip the download confirmation
// TODO(parthsareen): consolidate with cloud config changes
if strings.HasSuffix(model, "cloud") {
return pullModel(ctx, client, model)
}
if ok, err := confirmPrompt(fmt.Sprintf("Download %s?", model)); err != nil {
return err
} else if !ok {
@@ -670,24 +647,6 @@ func SaveIntegrationModel(name, modelName string) error {
return saveIntegration(name, models)
}
// SaveAndEditIntegration saves the models for an Editor integration and runs its Edit method
// to write the integration's config files.
func SaveAndEditIntegration(name string, models []string) error {
r, ok := integrations[strings.ToLower(name)]
if !ok {
return fmt.Errorf("unknown integration: %s", name)
}
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)
}
}
return nil
}
// 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]

View File

@@ -374,7 +374,7 @@ func TestParseArgs(t *testing.T) {
func TestIsCloudModel(t *testing.T) {
// isCloudModel now only uses Show API, so nil client always returns false
t.Run("nil client returns false", func(t *testing.T) {
models := []string{"glm-5:cloud", "kimi-k2.5:cloud", "local-model"}
models := []string{"glm-4.7:cloud", "kimi-k2.5:cloud", "local-model"}
for _, model := range models {
if isCloudModel(context.Background(), nil, model) {
t.Errorf("isCloudModel(%q) with nil client should return false", model)
@@ -394,7 +394,7 @@ func names(items []ModelItem) []string {
func TestBuildModelList_NoExistingModels(t *testing.T) {
items, _, _, _ := buildModelList(nil, nil, "")
want := []string{"glm-5:cloud", "kimi-k2.5:cloud", "glm-4.7-flash", "qwen3:8b"}
want := []string{"kimi-k2.5:cloud", "glm-4.7:cloud", "glm-4.7-flash", "qwen3:8b"}
if diff := cmp.Diff(want, names(items)); diff != "" {
t.Errorf("with no existing models, items should be recommended in order (-want +got):\n%s", diff)
}
@@ -416,7 +416,7 @@ func TestBuildModelList_OnlyLocalModels_CloudRecsAtBottom(t *testing.T) {
got := names(items)
// Recommended pinned at top (local recs first, then cloud recs when only-local), then installed non-recs
want := []string{"glm-4.7-flash", "qwen3:8b", "glm-5:cloud", "kimi-k2.5:cloud", "llama3.2", "qwen2.5"}
want := []string{"glm-4.7-flash", "qwen3:8b", "kimi-k2.5:cloud", "glm-4.7:cloud", "llama3.2", "qwen2.5"}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("recs pinned at top, local recs before cloud recs (-want +got):\n%s", diff)
}
@@ -425,14 +425,14 @@ func TestBuildModelList_OnlyLocalModels_CloudRecsAtBottom(t *testing.T) {
func TestBuildModelList_BothCloudAndLocal_RegularSort(t *testing.T) {
existing := []modelInfo{
{Name: "llama3.2:latest", Remote: false},
{Name: "glm-5:cloud", Remote: true},
{Name: "glm-4.7:cloud", Remote: true},
}
items, _, _, _ := buildModelList(existing, nil, "")
got := names(items)
// All recs pinned at top (cloud before local in mixed case), then non-recs
want := []string{"glm-5:cloud", "kimi-k2.5:cloud", "glm-4.7-flash", "qwen3:8b", "llama3.2"}
want := []string{"kimi-k2.5:cloud", "glm-4.7:cloud", "glm-4.7-flash", "qwen3:8b", "llama3.2"}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("recs pinned at top, cloud recs first in mixed case (-want +got):\n%s", diff)
}
@@ -441,7 +441,7 @@ func TestBuildModelList_BothCloudAndLocal_RegularSort(t *testing.T) {
func TestBuildModelList_PreCheckedFirst(t *testing.T) {
existing := []modelInfo{
{Name: "llama3.2:latest", Remote: false},
{Name: "glm-5:cloud", Remote: true},
{Name: "glm-4.7:cloud", Remote: true},
}
items, _, _, _ := buildModelList(existing, []string{"llama3.2"}, "")
@@ -455,14 +455,14 @@ func TestBuildModelList_PreCheckedFirst(t *testing.T) {
func TestBuildModelList_ExistingRecommendedMarked(t *testing.T) {
existing := []modelInfo{
{Name: "glm-4.7-flash", Remote: false},
{Name: "glm-5:cloud", Remote: true},
{Name: "glm-4.7:cloud", Remote: true},
}
items, _, _, _ := buildModelList(existing, nil, "")
for _, item := range items {
switch item.Name {
case "glm-4.7-flash", "glm-5:cloud":
case "glm-4.7-flash", "glm-4.7:cloud":
if strings.HasSuffix(item.Description, "install?") {
t.Errorf("installed recommended %q should not have 'install?' suffix, got %q", item.Name, item.Description)
}
@@ -477,16 +477,16 @@ func TestBuildModelList_ExistingRecommendedMarked(t *testing.T) {
func TestBuildModelList_ExistingCloudModelsNotPushedToBottom(t *testing.T) {
existing := []modelInfo{
{Name: "glm-4.7-flash", Remote: false},
{Name: "glm-5:cloud", Remote: true},
{Name: "glm-4.7:cloud", Remote: true},
}
items, _, _, _ := buildModelList(existing, nil, "")
got := names(items)
// glm-4.7-flash and glm-5:cloud are installed so they sort normally;
// glm-4.7-flash and glm-4.7:cloud are installed so they sort normally;
// kimi-k2.5:cloud and qwen3:8b are not installed so they go to the bottom
// All recs: cloud first in mixed case, then local, in rec order within each
want := []string{"glm-5:cloud", "kimi-k2.5:cloud", "glm-4.7-flash", "qwen3:8b"}
want := []string{"kimi-k2.5:cloud", "glm-4.7:cloud", "glm-4.7-flash", "qwen3:8b"}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("all recs, cloud first in mixed case (-want +got):\n%s", diff)
}
@@ -504,7 +504,7 @@ func TestBuildModelList_HasRecommendedCloudModel_OnlyNonInstalledAtBottom(t *tes
// kimi-k2.5:cloud is installed so it sorts normally;
// the rest of the recommendations are not installed so they go to the bottom
// All recs pinned at top (cloud first in mixed case), then non-recs
want := []string{"glm-5:cloud", "kimi-k2.5:cloud", "glm-4.7-flash", "qwen3:8b", "llama3.2"}
want := []string{"kimi-k2.5:cloud", "glm-4.7:cloud", "glm-4.7-flash", "qwen3:8b", "llama3.2"}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("recs pinned at top, cloud first in mixed case (-want +got):\n%s", diff)
}
@@ -554,7 +554,7 @@ func TestBuildModelList_LatestTagStripped(t *testing.T) {
func TestBuildModelList_ReturnsExistingAndCloudMaps(t *testing.T) {
existing := []modelInfo{
{Name: "llama3.2:latest", Remote: false},
{Name: "glm-5:cloud", Remote: true},
{Name: "glm-4.7:cloud", Remote: true},
}
_, _, existingModels, cloudModels := buildModelList(existing, nil, "")
@@ -562,15 +562,15 @@ func TestBuildModelList_ReturnsExistingAndCloudMaps(t *testing.T) {
if !existingModels["llama3.2"] {
t.Error("llama3.2 should be in existingModels")
}
if !existingModels["glm-5:cloud"] {
t.Error("glm-5:cloud should be in existingModels")
if !existingModels["glm-4.7:cloud"] {
t.Error("glm-4.7:cloud should be in existingModels")
}
if existingModels["glm-4.7-flash"] {
t.Error("glm-4.7-flash should not be in existingModels (it's a recommendation)")
}
if !cloudModels["glm-5:cloud"] {
t.Error("glm-5:cloud should be in cloudModels")
if !cloudModels["glm-4.7:cloud"] {
t.Error("glm-4.7:cloud should be in cloudModels")
}
if !cloudModels["kimi-k2.5:cloud"] {
t.Error("kimi-k2.5:cloud should be in cloudModels (recommended cloud)")
@@ -590,7 +590,7 @@ func TestBuildModelList_RecommendedFieldSet(t *testing.T) {
for _, item := range items {
switch item.Name {
case "glm-4.7-flash", "qwen3:8b", "glm-5:cloud", "kimi-k2.5:cloud":
case "glm-4.7-flash", "qwen3:8b", "glm-4.7:cloud", "kimi-k2.5:cloud":
if !item.Recommended {
t.Errorf("%q should have Recommended=true", item.Name)
}
@@ -605,14 +605,14 @@ func TestBuildModelList_RecommendedFieldSet(t *testing.T) {
func TestBuildModelList_MixedCase_CloudRecsFirst(t *testing.T) {
existing := []modelInfo{
{Name: "llama3.2:latest", Remote: false},
{Name: "glm-5:cloud", Remote: true},
{Name: "glm-4.7:cloud", Remote: true},
}
items, _, _, _ := buildModelList(existing, nil, "")
got := names(items)
// Cloud recs should sort before local recs in mixed case
cloudIdx := slices.Index(got, "glm-5:cloud")
cloudIdx := slices.Index(got, "glm-4.7:cloud")
localIdx := slices.Index(got, "glm-4.7-flash")
if cloudIdx > localIdx {
t.Errorf("cloud recs should be before local recs in mixed case, got %v", got)
@@ -629,7 +629,7 @@ func TestBuildModelList_OnlyLocal_LocalRecsFirst(t *testing.T) {
// Local recs should sort before cloud recs in only-local case
localIdx := slices.Index(got, "glm-4.7-flash")
cloudIdx := slices.Index(got, "glm-5:cloud")
cloudIdx := slices.Index(got, "glm-4.7:cloud")
if localIdx > cloudIdx {
t.Errorf("local recs should be before cloud recs in only-local case, got %v", got)
}
@@ -648,7 +648,7 @@ func TestBuildModelList_RecsAboveNonRecs(t *testing.T) {
lastRecIdx := -1
firstNonRecIdx := len(got)
for i, name := range got {
isRec := name == "glm-4.7-flash" || name == "qwen3:8b" || name == "glm-5:cloud" || name == "kimi-k2.5:cloud"
isRec := name == "glm-4.7-flash" || name == "qwen3:8b" || name == "glm-4.7:cloud" || name == "kimi-k2.5:cloud"
if isRec && i > lastRecIdx {
lastRecIdx = i
}
@@ -664,7 +664,7 @@ func TestBuildModelList_RecsAboveNonRecs(t *testing.T) {
func TestBuildModelList_CheckedBeforeRecs(t *testing.T) {
existing := []modelInfo{
{Name: "llama3.2:latest", Remote: false},
{Name: "glm-5:cloud", Remote: true},
{Name: "glm-4.7:cloud", Remote: true},
}
items, _, _, _ := buildModelList(existing, []string{"llama3.2"}, "")
@@ -843,43 +843,6 @@ func TestShowOrPull_ModelNotFound_ConfirmNo_Cancelled(t *testing.T) {
}
}
func TestShowOrPull_CloudModel_SkipsConfirmation(t *testing.T) {
// Confirm prompt should NOT be called for cloud models
oldHook := DefaultConfirmPrompt
DefaultConfirmPrompt = func(prompt string) (bool, error) {
t.Error("confirm prompt should not be called for cloud models")
return false, nil
}
defer func() { DefaultConfirmPrompt = oldHook }()
var pullCalled bool
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/show":
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, `{"error":"model not found"}`)
case "/api/pull":
pullCalled = true
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{"status":"success"}`)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
defer srv.Close()
u, _ := url.Parse(srv.URL)
client := api.NewClient(u, srv.Client())
err := ShowOrPull(context.Background(), client, "glm-5:cloud")
if err != nil {
t.Errorf("ShowOrPull should succeed for cloud model, got: %v", err)
}
if !pullCalled {
t.Error("expected pull to be called for cloud model without confirmation")
}
}
func TestConfirmPrompt_DelegatesToHook(t *testing.T) {
oldHook := DefaultConfirmPrompt
var hookCalled bool
@@ -1201,56 +1164,3 @@ func TestLaunchIntegration_NotConfigured(t *testing.T) {
t.Errorf("error should mention 'not configured', got: %v", err)
}
}
func TestIsEditorIntegration(t *testing.T) {
tests := []struct {
name string
want bool
}{
{"droid", true},
{"opencode", true},
{"openclaw", true},
{"claude", false},
{"codex", false},
{"nonexistent", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsEditorIntegration(tt.name); got != tt.want {
t.Errorf("IsEditorIntegration(%q) = %v, want %v", tt.name, got, tt.want)
}
})
}
}
func TestIntegrationModels(t *testing.T) {
tmpDir := t.TempDir()
setTestHome(t, tmpDir)
t.Run("returns nil when not configured", func(t *testing.T) {
if got := IntegrationModels("droid"); got != nil {
t.Errorf("expected nil, got %v", got)
}
})
t.Run("returns all saved models", func(t *testing.T) {
if err := saveIntegration("droid", []string{"llama3.2", "qwen3:8b"}); err != nil {
t.Fatal(err)
}
got := IntegrationModels("droid")
want := []string{"llama3.2", "qwen3:8b"}
if diff := cmp.Diff(want, got); diff != "" {
t.Errorf("IntegrationModels mismatch (-want +got):\n%s", diff)
}
})
}
func TestSaveAndEditIntegration_UnknownIntegration(t *testing.T) {
err := SaveAndEditIntegration("nonexistent", []string{"model"})
if err == nil {
t.Fatal("expected error for unknown integration")
}
if !strings.Contains(err.Error(), "unknown integration") {
t.Errorf("error should mention 'unknown integration', got: %v", err)
}
}

View File

@@ -1,5 +0,0 @@
//go:build !windows
package cmd
const defaultEditor = "vi"

View File

@@ -1,5 +0,0 @@
//go:build windows
package cmd
const defaultEditor = "edit"

View File

@@ -7,7 +7,6 @@ import (
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"slices"
@@ -80,7 +79,6 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
fmt.Fprintln(os.Stderr, " Ctrl + w Delete the word before the cursor")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, " Ctrl + l Clear the screen")
fmt.Fprintln(os.Stderr, " Ctrl + g Open default editor to compose a prompt")
fmt.Fprintln(os.Stderr, " Ctrl + c Stop the model from responding")
fmt.Fprintln(os.Stderr, " Ctrl + d Exit ollama (/bye)")
fmt.Fprintln(os.Stderr, "")
@@ -149,18 +147,6 @@ func generateInteractive(cmd *cobra.Command, opts runOptions) error {
scanner.Prompt.UseAlt = false
sb.Reset()
continue
case errors.Is(err, readline.ErrEditPrompt):
sb.Reset()
content, err := editInExternalEditor(line)
if err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
continue
}
if strings.TrimSpace(content) == "" {
continue
}
scanner.Prefill = content
continue
case err != nil:
return err
@@ -612,57 +598,6 @@ func extractFileData(input string) (string, []api.ImageData, error) {
return strings.TrimSpace(input), imgs, nil
}
func editInExternalEditor(content string) (string, error) {
editor := envconfig.Editor()
if editor == "" {
editor = os.Getenv("VISUAL")
}
if editor == "" {
editor = os.Getenv("EDITOR")
}
if editor == "" {
editor = defaultEditor
}
// Check that the editor binary exists
name := strings.Fields(editor)[0]
if _, err := exec.LookPath(name); err != nil {
return "", fmt.Errorf("editor %q not found, set OLLAMA_EDITOR to the path of your preferred editor", name)
}
tmpFile, err := os.CreateTemp("", "ollama-prompt-*.txt")
if err != nil {
return "", fmt.Errorf("creating temp file: %w", err)
}
defer os.Remove(tmpFile.Name())
if content != "" {
if _, err := tmpFile.WriteString(content); err != nil {
tmpFile.Close()
return "", fmt.Errorf("writing to temp file: %w", err)
}
}
tmpFile.Close()
args := strings.Fields(editor)
args = append(args, tmpFile.Name())
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("editor exited with error: %w", err)
}
data, err := os.ReadFile(tmpFile.Name())
if err != nil {
return "", fmt.Errorf("reading temp file: %w", err)
}
return strings.TrimRight(string(data), "\n"), nil
}
func getImageData(filePath string) ([]byte, error) {
file, err := os.Open(filePath)
if err != nil {

View File

@@ -7,7 +7,6 @@ import (
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
"github.com/ollama/ollama/cmd/config"
)
var (
@@ -35,6 +34,12 @@ var (
selectorInputStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "235", Dark: "252"})
selectorCheckboxStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "242", Dark: "246"})
selectorCheckboxCheckedStyle = lipgloss.NewStyle().
Bold(true)
selectorDefaultTagStyle = lipgloss.NewStyle().
Foreground(lipgloss.AdaptiveColor{Light: "242", Dark: "246"}).
Italic(true)
@@ -64,30 +69,6 @@ type SelectItem struct {
Recommended bool
}
// ConvertItems converts config.ModelItem slice to SelectItem slice.
func ConvertItems(items []config.ModelItem) []SelectItem {
out := make([]SelectItem, len(items))
for i, item := range items {
out[i] = SelectItem{Name: item.Name, Description: item.Description, Recommended: item.Recommended}
}
return out
}
// ReorderItems returns a copy with recommended items first, then non-recommended,
// preserving relative order within each group. This ensures the data order matches
// the visual section layout (Recommended / More).
func ReorderItems(items []SelectItem) []SelectItem {
var rec, other []SelectItem
for _, item := range items {
if item.Recommended {
rec = append(rec, item)
} else {
other = append(other, item)
}
}
return append(rec, other...)
}
// selectorModel is the bubbletea model for single selection.
type selectorModel struct {
title string
@@ -440,50 +421,6 @@ func (m multiSelectorModel) filteredItems() []SelectItem {
return result
}
// otherStart returns the index of the first non-recommended item in the filtered list.
func (m multiSelectorModel) otherStart() int {
if m.filter != "" {
return 0
}
filtered := m.filteredItems()
for i, item := range filtered {
if !item.Recommended {
return i
}
}
return len(filtered)
}
// updateScroll adjusts scrollOffset for section-based scrolling (matches single-select).
func (m *multiSelectorModel) updateScroll(otherStart int) {
if m.filter != "" {
if m.cursor < m.scrollOffset {
m.scrollOffset = m.cursor
}
if m.cursor >= m.scrollOffset+maxSelectorItems {
m.scrollOffset = m.cursor - maxSelectorItems + 1
}
return
}
if m.cursor < otherStart {
m.scrollOffset = 0
return
}
posInOthers := m.cursor - otherStart
maxOthers := maxSelectorItems - otherStart
if maxOthers < 3 {
maxOthers = 3
}
if posInOthers < m.scrollOffset {
m.scrollOffset = posInOthers
}
if posInOthers >= m.scrollOffset+maxOthers {
m.scrollOffset = posInOthers - maxOthers + 1
}
}
func (m *multiSelectorModel) toggleItem() {
filtered := m.filteredItems()
if len(filtered) == 0 || m.cursor >= len(filtered) {
@@ -545,13 +482,17 @@ func (m multiSelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.KeyUp:
if m.cursor > 0 {
m.cursor--
m.updateScroll(m.otherStart())
if m.cursor < m.scrollOffset {
m.scrollOffset = m.cursor
}
}
case tea.KeyDown:
if m.cursor < len(filtered)-1 {
m.cursor++
m.updateScroll(m.otherStart())
if m.cursor >= m.scrollOffset+maxSelectorItems {
m.scrollOffset = m.cursor - maxSelectorItems + 1
}
}
case tea.KeyPgUp:
@@ -559,14 +500,19 @@ func (m multiSelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if m.cursor < 0 {
m.cursor = 0
}
m.updateScroll(m.otherStart())
m.scrollOffset -= maxSelectorItems
if m.scrollOffset < 0 {
m.scrollOffset = 0
}
case tea.KeyPgDown:
m.cursor += maxSelectorItems
if m.cursor >= len(filtered) {
m.cursor = len(filtered) - 1
}
m.updateScroll(m.otherStart())
if m.cursor >= m.scrollOffset+maxSelectorItems {
m.scrollOffset = m.cursor - maxSelectorItems + 1
}
case tea.KeyBackspace:
if len(m.filter) > 0 {
@@ -585,34 +531,6 @@ func (m multiSelectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
func (m multiSelectorModel) renderMultiItem(s *strings.Builder, item SelectItem, idx int) {
origIdx := m.itemIndex[item.Name]
var check string
if m.checked[origIdx] {
check = "[x] "
} else {
check = "[ ] "
}
suffix := ""
if len(m.checkOrder) > 0 && m.checkOrder[0] == origIdx {
suffix = " " + selectorDefaultTagStyle.Render("(default)")
}
if idx == m.cursor {
s.WriteString(selectorSelectedItemStyle.Render("▸ " + check + item.Name))
} else {
s.WriteString(selectorItemStyle.Render(check + item.Name))
}
s.WriteString(suffix)
s.WriteString("\n")
if item.Description != "" {
s.WriteString(selectorDescLineStyle.Render(item.Description))
s.WriteString("\n")
}
}
func (m multiSelectorModel) View() string {
if m.cancelled || m.confirmed {
return ""
@@ -634,65 +552,56 @@ func (m multiSelectorModel) View() string {
if len(filtered) == 0 {
s.WriteString(selectorItemStyle.Render(selectorDescStyle.Render("(no matches)")))
s.WriteString("\n")
} else if m.filter != "" {
// Filtering: flat scroll through all matches
} else {
displayCount := min(len(filtered), maxSelectorItems)
shownRecHeader := false
prevWasRec := false
for i := range displayCount {
idx := m.scrollOffset + i
if idx >= len(filtered) {
break
}
m.renderMultiItem(&s, filtered[idx], idx)
item := filtered[idx]
origIdx := m.itemIndex[item.Name]
if m.filter == "" {
if item.Recommended && !shownRecHeader {
s.WriteString(sectionHeaderStyle.Render("Recommended"))
s.WriteString("\n")
shownRecHeader = true
} else if !item.Recommended && prevWasRec {
s.WriteString("\n")
}
prevWasRec = item.Recommended
}
var checkbox string
if m.checked[origIdx] {
checkbox = selectorCheckboxCheckedStyle.Render("[x]")
} else {
checkbox = selectorCheckboxStyle.Render("[ ]")
}
var line string
if idx == m.cursor {
line = selectorSelectedItemStyle.Render("▸ ") + checkbox + " " + selectorSelectedItemStyle.Render(item.Name)
} else {
line = " " + checkbox + " " + item.Name
}
if len(m.checkOrder) > 0 && m.checkOrder[0] == origIdx {
line += " " + selectorDefaultTagStyle.Render("(default)")
}
s.WriteString(line)
s.WriteString("\n")
}
if remaining := len(filtered) - m.scrollOffset - displayCount; remaining > 0 {
s.WriteString(selectorMoreStyle.Render(fmt.Sprintf("... and %d more", remaining)))
s.WriteString("\n")
}
} else {
// Split into pinned recommended and scrollable others (matches single-select layout)
var recItems, otherItems []int
for i, item := range filtered {
if item.Recommended {
recItems = append(recItems, i)
} else {
otherItems = append(otherItems, i)
}
}
// Always render all recommended items (pinned)
if len(recItems) > 0 {
s.WriteString(sectionHeaderStyle.Render("Recommended"))
s.WriteString("\n")
for _, idx := range recItems {
m.renderMultiItem(&s, filtered[idx], idx)
}
}
if len(otherItems) > 0 {
s.WriteString("\n")
s.WriteString(sectionHeaderStyle.Render("More"))
s.WriteString("\n")
maxOthers := maxSelectorItems - len(recItems)
if maxOthers < 3 {
maxOthers = 3
}
displayCount := min(len(otherItems), maxOthers)
for i := range displayCount {
idx := m.scrollOffset + i
if idx >= len(otherItems) {
break
}
m.renderMultiItem(&s, filtered[otherItems[idx]], otherItems[idx])
}
if remaining := len(otherItems) - m.scrollOffset - displayCount; remaining > 0 {
s.WriteString(selectorMoreStyle.Render(fmt.Sprintf("... and %d more", remaining)))
s.WriteString("\n")
}
}
}
s.WriteString("\n")

View File

@@ -382,169 +382,6 @@ func TestUpdateNavigation_Backspace(t *testing.T) {
}
}
// --- ReorderItems ---
func TestReorderItems(t *testing.T) {
input := []SelectItem{
{Name: "local-1"},
{Name: "rec-a", Recommended: true},
{Name: "local-2"},
{Name: "rec-b", Recommended: true},
}
got := ReorderItems(input)
want := []string{"rec-a", "rec-b", "local-1", "local-2"}
for i, item := range got {
if item.Name != want[i] {
t.Errorf("index %d: got %q, want %q", i, item.Name, want[i])
}
}
}
func TestReorderItems_AllRecommended(t *testing.T) {
input := recItems("a", "b", "c")
got := ReorderItems(input)
if len(got) != 3 {
t.Fatalf("expected 3 items, got %d", len(got))
}
for i, item := range got {
if item.Name != input[i].Name {
t.Errorf("order should be preserved, index %d: got %q, want %q", i, item.Name, input[i].Name)
}
}
}
func TestReorderItems_NoneRecommended(t *testing.T) {
input := items("x", "y")
got := ReorderItems(input)
if len(got) != 2 || got[0].Name != "x" || got[1].Name != "y" {
t.Errorf("order should be preserved, got %v", got)
}
}
// --- Multi-select otherStart ---
func TestMultiOtherStart(t *testing.T) {
tests := []struct {
name string
items []SelectItem
filter string
want int
}{
{"all recommended", recItems("a", "b"), "", 2},
{"none recommended", items("a", "b"), "", 0},
{"mixed", mixedItems(), "", 2},
{"with filter returns 0", mixedItems(), "other", 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := newMultiSelectorModel("test", tt.items, nil)
m.filter = tt.filter
if got := m.otherStart(); got != tt.want {
t.Errorf("otherStart() = %d, want %d", got, tt.want)
}
})
}
}
// --- Multi-select updateScroll ---
func TestMultiUpdateScroll(t *testing.T) {
tests := []struct {
name string
cursor int
offset int
otherStart int
wantOffset int
}{
{"cursor in recommended resets scroll", 1, 5, 3, 0},
{"cursor at start of others", 2, 0, 2, 0},
{"cursor scrolls down in others", 12, 0, 2, 3},
{"cursor scrolls up in others", 4, 5, 2, 2},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m := newMultiSelectorModel("test", nil, nil)
m.cursor = tt.cursor
m.scrollOffset = tt.offset
m.updateScroll(tt.otherStart)
if m.scrollOffset != tt.wantOffset {
t.Errorf("scrollOffset = %d, want %d", m.scrollOffset, tt.wantOffset)
}
})
}
}
// --- Multi-select View section headers ---
func TestMultiView_SectionHeaders(t *testing.T) {
m := newMultiSelectorModel("Pick:", []SelectItem{
{Name: "rec-a", Recommended: true},
{Name: "other-1"},
}, nil)
content := m.View()
if !strings.Contains(content, "Recommended") {
t.Error("should contain 'Recommended' header")
}
if !strings.Contains(content, "More") {
t.Error("should contain 'More' header")
}
}
func TestMultiView_CursorIndicator(t *testing.T) {
m := newMultiSelectorModel("Pick:", items("a", "b"), nil)
m.cursor = 0
content := m.View()
if !strings.Contains(content, "▸") {
t.Error("should show ▸ cursor indicator")
}
}
func TestMultiView_CheckedItemShowsX(t *testing.T) {
m := newMultiSelectorModel("Pick:", items("a", "b"), []string{"a"})
content := m.View()
if !strings.Contains(content, "[x]") {
t.Error("checked item should show [x]")
}
if !strings.Contains(content, "[ ]") {
t.Error("unchecked item should show [ ]")
}
}
func TestMultiView_DefaultTag(t *testing.T) {
m := newMultiSelectorModel("Pick:", items("a", "b"), []string{"a"})
content := m.View()
if !strings.Contains(content, "(default)") {
t.Error("first checked item should have (default) tag")
}
}
func TestMultiView_PinnedRecommended(t *testing.T) {
m := newMultiSelectorModel("Pick:", mixedItems(), nil)
m.cursor = 8
m.scrollOffset = 3
content := m.View()
if !strings.Contains(content, "rec-a") {
t.Error("recommended items should always be visible (pinned)")
}
if !strings.Contains(content, "rec-b") {
t.Error("recommended items should always be visible (pinned)")
}
}
func TestMultiView_OverflowIndicator(t *testing.T) {
m := newMultiSelectorModel("Pick:", mixedItems(), nil)
content := m.View()
if !strings.Contains(content, "... and") {
t.Error("should show overflow indicator when more items than visible")
}
}
// Key message helpers for testing
type keyType = int

View File

@@ -115,7 +115,6 @@ type model struct {
quitting bool
selected bool
changeModel bool
changeModels []string // multi-select result for Editor integrations
showOthers bool
availableModels map[string]bool
err error
@@ -124,9 +123,6 @@ type model struct {
modalSelector selectorModel
modalItems []SelectItem
showingMultiModal bool
multiModalSelector multiSelectorModel
showingSignIn bool
signInURL string
signInModel string
@@ -164,50 +160,23 @@ func (m *model) modelExists(name string) bool {
func (m *model) buildModalItems() []SelectItem {
modelItems, _ := config.GetModelItems(context.Background())
return ReorderItems(ConvertItems(modelItems))
var items []SelectItem
for _, item := range modelItems {
items = append(items, SelectItem{Name: item.Name, Description: item.Description, Recommended: item.Recommended})
}
return items
}
func (m *model) openModelModal(currentModel string) {
func (m *model) openModelModal() {
m.modalItems = m.buildModalItems()
cursor := 0
if currentModel != "" {
for i, item := range m.modalItems {
if item.Name == currentModel || strings.HasPrefix(item.Name, currentModel+":") || strings.HasPrefix(currentModel, item.Name+":") {
cursor = i
break
}
}
}
m.modalSelector = selectorModel{
title: "Select model:",
items: m.modalItems,
cursor: cursor,
helpText: "↑/↓ navigate • enter select • ← back",
}
m.modalSelector.updateScroll(m.modalSelector.otherStart())
m.showingModal = true
}
func (m *model) openMultiModelModal(integration string) {
items := m.buildModalItems()
var preChecked []string
if models := config.IntegrationModels(integration); len(models) > 0 {
preChecked = models
}
m.multiModalSelector = newMultiSelectorModel("Select models:", items, preChecked)
// Set cursor to the first pre-checked (last used) model
if len(preChecked) > 0 {
for i, item := range items {
if item.Name == preChecked[0] {
m.multiModalSelector.cursor = i
m.multiModalSelector.updateScroll(m.multiModalSelector.otherStart())
break
}
}
}
m.showingMultiModal = true
}
func isCloudModel(name string) bool {
return strings.HasSuffix(name, ":cloud")
}
@@ -387,39 +356,6 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
if m.showingMultiModal {
switch msg := msg.(type) {
case tea.KeyMsg:
if msg.Type == tea.KeyLeft {
m.showingMultiModal = false
return m, nil
}
updated, cmd := m.multiModalSelector.Update(msg)
m.multiModalSelector = updated.(multiSelectorModel)
if m.multiModalSelector.cancelled {
m.showingMultiModal = false
return m, nil
}
if m.multiModalSelector.confirmed {
var selected []string
for _, idx := range m.multiModalSelector.checkOrder {
selected = append(selected, m.multiModalSelector.items[idx].Name)
}
if len(selected) > 0 {
m.changeModels = selected
m.changeModel = true
m.quitting = true
return m, tea.Quit
}
m.multiModalSelector.confirmed = false
return m, nil
}
return m, cmd
}
return m, nil
}
if m.showingModal {
switch msg := msg.(type) {
case tea.KeyMsg:
@@ -506,17 +442,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if item.integration != "" && !config.IsIntegrationInstalled(item.integration) {
return m, nil
}
if item.integration != "" && config.IsEditorIntegration(item.integration) {
m.openMultiModelModal(item.integration)
} else {
var currentModel string
if item.isRunModel {
currentModel = config.LastModel()
} else if item.integration != "" {
currentModel = config.IntegrationModel(item.integration)
}
m.openModelModal(currentModel)
}
m.openModelModal()
}
}
}
@@ -533,10 +459,6 @@ func (m model) View() string {
return m.renderSignInDialog()
}
if m.showingMultiModal {
return m.multiModalSelector.View()
}
if m.showingModal {
return m.renderModal()
}
@@ -632,9 +554,8 @@ const (
type Result struct {
Selection Selection
Integration string // integration name if applicable
Model string // model name if selected from single-select modal
Models []string // models selected from multi-select modal (Editor integrations)
Integration string // integration name if applicable
Model string // model name if selected from modal
}
func Run() (Result, error) {
@@ -668,7 +589,6 @@ func Run() (Result, error) {
Selection: SelectionChangeIntegration,
Integration: item.integration,
Model: fm.modalSelector.selected,
Models: fm.changeModels,
}, nil
}

View File

@@ -596,15 +596,6 @@ components:
name:
type: string
description: Model name
model:
type: string
description: Model name
remote_model:
type: string
description: Name of the upstream model, if the model is remote
remote_host:
type: string
description: URL of the upstream Ollama host, if the model is remote
modified_at:
type: string
description: Last modified timestamp in ISO 8601 format
@@ -645,9 +636,6 @@ components:
Ps:
type: object
properties:
name:
type: string
description: Name of the running model
model:
type: string
description: Name of the running model
@@ -1149,7 +1137,6 @@ paths:
example:
models:
- name: "gemma3"
model: "gemma3"
modified_at: "2025-10-03T23:34:03.409490317-07:00"
size: 3338801804
digest: "a2af6cc3eb7fa8be8504abaf9b04e88f17a119ec3f04a3addf55f92841195f5a"
@@ -1181,8 +1168,7 @@ paths:
$ref: "#/components/schemas/PsResponse"
example:
models:
- name: "gemma3"
model: "gemma3"
- model: "gemma3"
size: 6591830464
digest: "a2af6cc3eb7fa8be8504abaf9b04e88f17a119ec3f04a3addf55f92841195f5a"
details:

View File

@@ -2,7 +2,7 @@
title: Quickstart
---
This quickstart will walk your through running your first model with Ollama. To get started, download Ollama on macOS, Windows or Linux.
Ollama is available on macOS, Windows, and Linux.
<a
href="https://ollama.com/download"
@@ -12,131 +12,48 @@ This quickstart will walk your through running your first model with Ollama. To
Download Ollama
</a>
## Run a model
## Get Started
<Tabs>
<Tab title="CLI">
Open a terminal and run the command:
```sh
ollama run gemma3
```
</Tab>
<Tab title="cURL">
```sh
ollama pull gemma3
```
Lastly, chat with the model:
```shell
curl http://localhost:11434/api/chat -d '{
"model": "gemma3",
"messages": [{
"role": "user",
"content": "Hello there!"
}],
"stream": false
}'
```
</Tab>
<Tab title="Python">
Start by downloading a model:
```sh
ollama pull gemma3
```
Then install Ollama's Python library:
```sh
pip install ollama
```
Lastly, chat with the model:
```python
from ollama import chat
from ollama import ChatResponse
response: ChatResponse = chat(model='gemma3', messages=[
{
'role': 'user',
'content': 'Why is the sky blue?',
},
])
print(response['message']['content'])
# or access fields directly from the response object
print(response.message.content)
```
</Tab>
<Tab title="JavaScript">
Start by downloading a model:
```
ollama pull gemma3
```
Then install the Ollama JavaScript library:
```
npm i ollama
```
Lastly, chat with the model:
```shell
import ollama from 'ollama'
const response = await ollama.chat({
model: 'gemma3',
messages: [{ role: 'user', content: 'Why is the sky blue?' }],
})
console.log(response.message.content)
```
</Tab>
</Tabs>
See a full list of available models [here](https://ollama.com/models).
## Coding
For coding use cases, we recommend using the `glm-4.7-flash` model.
Note: this model requires 23 GB of VRAM with 64000 tokens context length.
```sh
ollama pull glm-4.7-flash
```
Alternatively, you can use a more powerful cloud model (with full context length):
```sh
ollama pull glm-4.7:cloud
```
Use `ollama launch` to quickly set up a coding tool with Ollama models:
Run `ollama` in your terminal to open the interactive menu:
```sh
ollama launch
ollama
```
### Supported integrations
Navigate with `↑/↓`, press `enter` to launch, `→` to change model, and `esc` to quit.
- [OpenCode](/integrations/opencode) - Open-source coding assistant
- [Claude Code](/integrations/claude-code) - Anthropic's agentic coding tool
- [Codex](/integrations/codex) - OpenAI's coding assistant
- [Droid](/integrations/droid) - Factory's AI coding agent
The menu provides quick access to:
- **Run a model** - Start an interactive chat
- **Launch tools** - Claude Code, Codex, OpenClaw, and more
- **Additional integrations** - Available under "More..."
### Launch with a specific model
## Coding
Launch coding tools with Ollama models:
```sh
ollama launch claude --model glm-4.7-flash
ollama launch claude
```
### Configure without launching
```sh
ollama launch claude --config
ollama launch codex
```
```sh
ollama launch opencode
```
See [integrations](/integrations) for all supported tools.
## API
Use the [API](/api) to integrate Ollama into your applications:
```sh
curl http://localhost:11434/api/chat -d '{
"model": "gemma3",
"messages": [{ "role": "user", "content": "Hello!" }]
}'
```
See the [API documentation](/api) for Python, JavaScript, and other integrations.

View File

@@ -216,7 +216,6 @@ func String(s string) func() string {
var (
LLMLibrary = String("OLLAMA_LLM_LIBRARY")
Editor = String("OLLAMA_EDITOR")
CudaVisibleDevices = String("CUDA_VISIBLE_DEVICES")
HipVisibleDevices = String("HIP_VISIBLE_DEVICES")
@@ -292,7 +291,6 @@ func AsMap() map[string]EnvVar {
"OLLAMA_SCHED_SPREAD": {"OLLAMA_SCHED_SPREAD", SchedSpread(), "Always schedule model across all GPUs"},
"OLLAMA_MULTIUSER_CACHE": {"OLLAMA_MULTIUSER_CACHE", MultiUserCache(), "Optimize prompt caching for multi-user scenarios"},
"OLLAMA_CONTEXT_LENGTH": {"OLLAMA_CONTEXT_LENGTH", ContextLength(), "Context length to use unless otherwise specified (default: 4k/32k/256k based on VRAM)"},
"OLLAMA_EDITOR": {"OLLAMA_EDITOR", Editor(), "Path to editor for interactive prompt editing (Ctrl+G)"},
"OLLAMA_NEW_ENGINE": {"OLLAMA_NEW_ENGINE", NewEngine(), "Enable the new Ollama engine"},
"OLLAMA_REMOTES": {"OLLAMA_REMOTES", Remotes(), "Allowed hosts for remote models (default \"ollama.com\")"},

View File

@@ -5,7 +5,6 @@ import (
)
var ErrInterrupt = errors.New("Interrupt")
var ErrEditPrompt = errors.New("EditPrompt")
type InterruptError struct {
Line []rune

View File

@@ -41,7 +41,6 @@ type Instance struct {
Terminal *Terminal
History *History
Pasting bool
Prefill string
pastedLines []string
}
@@ -90,27 +89,6 @@ func (i *Instance) Readline() (string, error) {
buf, _ := NewBuffer(i.Prompt)
// Prefill the buffer with any text that we received from an external editor
if i.Prefill != "" {
lines := strings.Split(i.Prefill, "\n")
i.Prefill = ""
for idx, l := range lines {
for _, r := range l {
buf.Add(r)
}
if idx < len(lines)-1 {
i.pastedLines = append(i.pastedLines, buf.String())
buf.Buf.Clear()
buf.Pos = 0
buf.DisplayPos = 0
buf.LineHasSpace.Clear()
fmt.Println()
fmt.Print(i.Prompt.AltPrompt)
i.Prompt.UseAlt = true
}
}
}
var esc bool
var escex bool
var metaDel bool
@@ -273,29 +251,6 @@ func (i *Instance) Readline() (string, error) {
buf.ClearScreen()
case CharCtrlW:
buf.DeleteWord()
case CharBell:
output := buf.String()
numPastedLines := len(i.pastedLines)
if numPastedLines > 0 {
output = strings.Join(i.pastedLines, "\n") + "\n" + output
i.pastedLines = nil
}
// Move cursor to the last display line of the current buffer
currLine := buf.DisplayPos / buf.LineWidth
lastLine := buf.DisplaySize() / buf.LineWidth
if lastLine > currLine {
fmt.Print(CursorDownN(lastLine - currLine))
}
// Clear all lines from bottom to top: buffer wrapped lines + pasted lines
for range lastLine + numPastedLines {
fmt.Print(CursorBOL + ClearToEOL + CursorUp)
}
fmt.Print(CursorBOL + ClearToEOL)
i.Prompt.UseAlt = false
return output, ErrEditPrompt
case CharCtrlZ:
fd := os.Stdin.Fd()
return handleCharCtrlZ(fd, i.Terminal.termios)

View File

@@ -8,7 +8,6 @@ package mlx
import "C"
import (
"fmt"
"io/fs"
"log/slog"
"os"
@@ -17,13 +16,6 @@ import (
"unsafe"
)
var initError error
// CheckInit returns any error that occurred during MLX dynamic library initialization.
func CheckInit() error {
return initError
}
func init() {
switch runtime.GOOS {
case "darwin":
@@ -42,9 +34,7 @@ func init() {
for _, path := range filepath.SplitList(paths) {
matches, err := fs.Glob(os.DirFS(path), "libmlxc.*")
if err != nil {
initError = fmt.Errorf("failed to glob for MLX libraries in %s: %w", path, err)
slog.Warn("MLX dynamic library not available", "error", initError)
return
panic(err)
}
for _, match := range matches {
@@ -71,6 +61,5 @@ func init() {
}
}
initError = fmt.Errorf("failed to load any MLX dynamic library from OLLAMA_LIBRARY_PATH=%s", paths)
slog.Warn("MLX dynamic library not available", "error", initError)
panic("Failed to load any MLX dynamic library")
}

View File

@@ -7,7 +7,6 @@ import (
"cmp"
"encoding/json"
"flag"
"fmt"
"io"
"log/slog"
"net/http"
@@ -17,17 +16,12 @@ import (
"github.com/ollama/ollama/envconfig"
"github.com/ollama/ollama/logutil"
"github.com/ollama/ollama/x/mlxrunner/mlx"
"github.com/ollama/ollama/x/mlxrunner/sample"
)
func Execute(args []string) error {
slog.SetDefault(logutil.NewLogger(os.Stderr, envconfig.LogLevel()))
if err := mlx.CheckInit(); err != nil {
return fmt.Errorf("MLX not available: %w", err)
}
var (
modelName string
port int