mirror of
https://github.com/ollama/ollama.git
synced 2026-09-21 13:38:14 -05:00
app: always enable Claude auto mode
This commit is contained in:
+9
-105
@@ -66,12 +66,12 @@ const (
|
||||
// claudeDesktopController abstracts launch's Claude Desktop profile management
|
||||
// so app flows can be tested without probing a live gateway.
|
||||
type claudeDesktopController interface {
|
||||
AutodiscoveryConfiguredWithAutoMode(autoMode bool) bool
|
||||
AutodiscoveryConfigured() bool
|
||||
UsesOllamaGateway() bool
|
||||
Running() bool
|
||||
Open() error
|
||||
ConfigureAutodiscoveryWithAutoMode(autoMode bool) error
|
||||
SetInstalledFromDesktopWithAutoMode(installed, restart, autoMode bool) error
|
||||
ConfigureAutodiscovery() error
|
||||
SetInstalledFromDesktop(installed, restart bool) error
|
||||
ApplyProfileChange(change func() error, restartConfirmed bool) error
|
||||
RestoreForShutdown(ctx context.Context) error
|
||||
}
|
||||
@@ -555,11 +555,7 @@ func reconcileClaudeAppProxy() error {
|
||||
}
|
||||
|
||||
func updateClaudeDesktopProfile() error {
|
||||
autoMode, err := effectiveClaudeDesktopAutoMode(activeClaudeDesktopModels())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if claudeDesktop.AutodiscoveryConfiguredWithAutoMode(autoMode) {
|
||||
if claudeDesktop.AutodiscoveryConfigured() {
|
||||
return nil
|
||||
}
|
||||
// Claude writes settings during shutdown. Do not race that write during
|
||||
@@ -567,7 +563,7 @@ func updateClaudeDesktopProfile() error {
|
||||
if claudeDesktopRunning() {
|
||||
return nil
|
||||
}
|
||||
if err := claudeDesktop.ConfigureAutodiscoveryWithAutoMode(autoMode); err != nil {
|
||||
if err := claudeDesktop.ConfigureAutodiscovery(); err != nil {
|
||||
return fmt.Errorf("update Claude Desktop profile: %w", err)
|
||||
}
|
||||
return nil
|
||||
@@ -865,44 +861,6 @@ func fallbackClaudeDesktopModels() []proxy.ClaudeDesktopModel {
|
||||
return models
|
||||
}
|
||||
|
||||
func claudeDesktopModelSupportsAutoMode(model proxy.ClaudeDesktopModel) bool {
|
||||
return model.AccountCloud
|
||||
}
|
||||
|
||||
func claudeDesktopModelsSupportAutoMode(models []proxy.ClaudeDesktopModel) bool {
|
||||
for _, model := range models {
|
||||
if claudeDesktopModelSupportsAutoMode(model) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func activeClaudeDesktopModels() []proxy.ClaudeDesktopModel {
|
||||
claudeProxyMu.Lock()
|
||||
gateway := claudeAppProxy
|
||||
available := append([]proxy.ClaudeDesktopModel(nil), claudeAvailableModels...)
|
||||
claudeProxyMu.Unlock()
|
||||
if gateway != nil {
|
||||
if models := gateway.Models(); len(models) > 0 {
|
||||
return models
|
||||
}
|
||||
}
|
||||
selected := proxy.SelectClaudeDesktopModels(available, launch.ClaudeDesktopModels())
|
||||
if len(selected) > 0 {
|
||||
return selected
|
||||
}
|
||||
return available
|
||||
}
|
||||
|
||||
func effectiveClaudeDesktopAutoMode(models []proxy.ClaudeDesktopModel) (bool, error) {
|
||||
enabled, err := launch.ClaudeDesktopAutoModeEnabled()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return enabled && claudeDesktopModelsSupportAutoMode(models), nil
|
||||
}
|
||||
|
||||
func includeSelectedClaudeDesktopModels(available, selected []proxy.ClaudeDesktopModel) []proxy.ClaudeDesktopModel {
|
||||
models := append([]proxy.ClaudeDesktopModel(nil), available...)
|
||||
seen := make(map[string]struct{}, len(models))
|
||||
@@ -1209,15 +1167,7 @@ func setClaudeGatewayInstalled(installed, restart bool) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
autoMode := false
|
||||
if installed {
|
||||
var err error
|
||||
autoMode, err = effectiveClaudeDesktopAutoMode(activeClaudeDesktopModels())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err := claudeDesktop.SetInstalledFromDesktopWithAutoMode(installed, restart, autoMode)
|
||||
err := claudeDesktop.SetInstalledFromDesktop(installed, restart)
|
||||
if !claudeDesktop.UsesOllamaGateway() {
|
||||
stopClaudeAppProxy()
|
||||
}
|
||||
@@ -1385,7 +1335,6 @@ func getClaudeDesktopConnectionStatus() claudeDesktopStatus {
|
||||
Description: model.Description,
|
||||
Cloud: model.Cloud,
|
||||
Selected: isSelected,
|
||||
AutoMode: claudeDesktopModelSupportsAutoMode(model),
|
||||
Availability: access.Availability,
|
||||
Reason: access.Reason,
|
||||
RequiredPlan: access.RequiredPlan,
|
||||
@@ -1405,14 +1354,9 @@ func getClaudeDesktopConnectionStatus() claudeDesktopStatus {
|
||||
}
|
||||
|
||||
status := claudeDesktopConnectionSummary(used)
|
||||
autoMode, autoModeErr := launch.ClaudeDesktopAutoModeEnabled()
|
||||
status.AutoMode = autoMode
|
||||
status.ModelSource = modelSource
|
||||
status.Models = modelStatuses
|
||||
status.Mappings = mappingStatuses
|
||||
if status.Error == "" && autoModeErr != nil {
|
||||
status.Error = autoModeErr.Error()
|
||||
}
|
||||
return status
|
||||
}
|
||||
|
||||
@@ -1439,10 +1383,7 @@ func prepareClaudeDesktopConnection() error {
|
||||
if err := startClaudeAppProxy(); err != nil {
|
||||
return err
|
||||
}
|
||||
autoMode, err := effectiveClaudeDesktopAutoMode(activeClaudeDesktopModels())
|
||||
if err == nil {
|
||||
err = claudeDesktop.ConfigureAutodiscoveryWithAutoMode(autoMode)
|
||||
}
|
||||
err := claudeDesktop.ConfigureAutodiscovery()
|
||||
if !claudeDesktop.UsesOllamaGateway() {
|
||||
stopClaudeAppProxy()
|
||||
}
|
||||
@@ -1453,35 +1394,6 @@ func openClaudeDesktopApplication() error {
|
||||
return launch.OpenClaudeDesktop()
|
||||
}
|
||||
|
||||
func setClaudeDesktopAutoMode(enabled, restartConfirmed bool) error {
|
||||
models := activeClaudeDesktopModels()
|
||||
if enabled && !claudeDesktopModelsSupportAutoMode(models) {
|
||||
return errors.New("select at least one cloud model available to your Ollama.com account")
|
||||
}
|
||||
previous, err := launch.ClaudeDesktopAutoModeEnabled()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if previous == enabled && (!claudeDesktop.UsesOllamaGateway() || claudeDesktop.AutodiscoveryConfiguredWithAutoMode(enabled)) {
|
||||
return nil
|
||||
}
|
||||
if !claudeDesktop.UsesOllamaGateway() {
|
||||
// The preference takes effect the next time the profile is written.
|
||||
if err := launch.SaveClaudeDesktopAutoMode(enabled); err != nil {
|
||||
return fmt.Errorf("save Claude Desktop auto mode: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return claudeDesktop.ApplyProfileChange(func() error {
|
||||
// Persist only after the native layer has established that a running
|
||||
// Claude process may be restarted. Canceling consent must be a no-op.
|
||||
if err := launch.SaveClaudeDesktopAutoMode(enabled); err != nil {
|
||||
return fmt.Errorf("save Claude Desktop auto mode: %w", err)
|
||||
}
|
||||
return claudeDesktop.ConfigureAutodiscoveryWithAutoMode(enabled)
|
||||
}, restartConfirmed)
|
||||
}
|
||||
|
||||
func applyClaudeDesktopMappings(mappings map[string]string, restartConfirmed bool) (bool, error) {
|
||||
return applyClaudeDesktopMappingsWithOpen(mappings, restartConfirmed, true)
|
||||
}
|
||||
@@ -1609,11 +1521,7 @@ func applyClaudeDesktopMappingsWithOpen(mappings map[string]string, restartConfi
|
||||
if err := startClaudeAppProxy(); err != nil {
|
||||
return err
|
||||
}
|
||||
autoMode, err := effectiveClaudeDesktopAutoMode(selected)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return claudeDesktop.ConfigureAutodiscoveryWithAutoMode(autoMode)
|
||||
return claudeDesktop.ConfigureAutodiscovery()
|
||||
}
|
||||
if err := claudeDesktop.ApplyProfileChange(applyInitialChange, restartConfirmed); err != nil {
|
||||
if errors.Is(err, launch.ErrClaudeDesktopRestartConfirmationRequired) {
|
||||
@@ -1645,11 +1553,7 @@ func applyClaudeDesktopMappingsWithOpen(mappings map[string]string, restartConfi
|
||||
claudeModelSource = "user"
|
||||
claudeProxyMu.Unlock()
|
||||
}
|
||||
autoMode, err := effectiveClaudeDesktopAutoMode(selected)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := claudeDesktop.ConfigureAutodiscoveryWithAutoMode(autoMode); err != nil {
|
||||
if err := claudeDesktop.ConfigureAutodiscovery(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -196,7 +196,7 @@ func TestClaudeEndpointRequestsAreNotSentUnsigned(t *testing.T) {
|
||||
}
|
||||
for _, model := range models {
|
||||
if model.Recommended {
|
||||
t.Fatalf("offline fallback model %q must not enable Auto mode", model.Name)
|
||||
t.Fatalf("offline fallback model %q must not be marked as an endpoint recommendation", model.Name)
|
||||
}
|
||||
}
|
||||
if called {
|
||||
@@ -330,7 +330,7 @@ func TestResolveClaudeDesktopStartupCatalogPreservesPersistedRouteMappings(t *te
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveClaudeDesktopStartupCatalogMarksDefaultAccountModelsAutoEligible(t *testing.T) {
|
||||
func TestResolveClaudeDesktopStartupCatalogVerifiesDefaultAccountModels(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
previousLoader := claudeModelsLoader
|
||||
previousResolver := claudeCloudModelsResolver
|
||||
@@ -352,8 +352,8 @@ func TestResolveClaudeDesktopStartupCatalogMarksDefaultAccountModelsAutoEligible
|
||||
if source != "endpoint" || len(available) != 1 || len(selected) != 1 {
|
||||
t.Fatalf("catalog = %+v selected = %+v source = %q", available, selected, source)
|
||||
}
|
||||
if !claudeDesktopModelsSupportAutoMode(selected) {
|
||||
t.Fatal("default account cloud model was not Auto-eligible")
|
||||
if !selected[0].AccountCloud {
|
||||
t.Fatal("default account cloud model was not verified")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -630,9 +630,6 @@ func TestResolveClaudeDesktopStartupCatalogVerifiesFallbackFromAccountInventory(
|
||||
if access.Availability != proxy.ClaudeDesktopAvailabilityAvailable {
|
||||
t.Fatalf("fallback account model access = %+v, want available", access)
|
||||
}
|
||||
if !claudeDesktopModelsSupportAutoMode(selected) {
|
||||
t.Fatal("verified fallback account model was not Auto-eligible")
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveClaudeDesktopStartupCatalogDoesNotListCloudWhenOff(t *testing.T) {
|
||||
@@ -989,7 +986,6 @@ type fakeClaudeDesktopController struct {
|
||||
modelsAtSet []string
|
||||
configureOnSet bool
|
||||
requireRestart bool
|
||||
autoMode bool
|
||||
restoreCalls int
|
||||
}
|
||||
|
||||
@@ -1004,11 +1000,11 @@ func (f *fakeClaudeDesktopController) Open() error {
|
||||
return f.setErr
|
||||
}
|
||||
|
||||
func (f *fakeClaudeDesktopController) AutodiscoveryConfiguredWithAutoMode(autoMode bool) bool {
|
||||
return f.configured && f.profileCurrent && f.autoMode == autoMode
|
||||
func (f *fakeClaudeDesktopController) AutodiscoveryConfigured() bool {
|
||||
return f.configured && f.profileCurrent
|
||||
}
|
||||
|
||||
func (f *fakeClaudeDesktopController) ConfigureAutodiscoveryWithAutoMode(autoMode bool) error {
|
||||
func (f *fakeClaudeDesktopController) ConfigureAutodiscovery() error {
|
||||
f.configureCalls++
|
||||
if f.configureErr != nil {
|
||||
f.profileCurrent = false
|
||||
@@ -1016,17 +1012,15 @@ func (f *fakeClaudeDesktopController) ConfigureAutodiscoveryWithAutoMode(autoMod
|
||||
}
|
||||
f.configured = true
|
||||
f.profileCurrent = true
|
||||
f.autoMode = autoMode
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeClaudeDesktopController) SetInstalledFromDesktopWithAutoMode(installed, restart, autoMode bool) error {
|
||||
func (f *fakeClaudeDesktopController) SetInstalledFromDesktop(installed, restart bool) error {
|
||||
if f.requireRestart && !restart {
|
||||
return errors.New("Claude Desktop restart confirmation is required before changing its profile")
|
||||
}
|
||||
f.installed = installed
|
||||
f.restart = restart
|
||||
f.autoMode = autoMode
|
||||
f.modelsAtSet = launch.ClaudeDesktopModels()
|
||||
if f.configureOnSet {
|
||||
f.configured = installed
|
||||
@@ -1122,199 +1116,6 @@ func TestUpdateClaudeDesktopProfileDefersRepairWhileClaudeRuns(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetClaudeDesktopAutoModePersistsUntilConnection(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
previousDesktop := claudeDesktop
|
||||
previousRunning := claudeDesktopRunning
|
||||
fake := &fakeClaudeDesktopController{}
|
||||
claudeDesktop = fake
|
||||
claudeDesktopRunning = func() bool {
|
||||
t.Fatal("disconnected preference change should not inspect the Claude process")
|
||||
return false
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
claudeDesktop = previousDesktop
|
||||
claudeDesktopRunning = previousRunning
|
||||
})
|
||||
|
||||
if err := setClaudeDesktopAutoMode(false, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
enabled, err := launch.ClaudeDesktopAutoModeEnabled()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if enabled || fake.configureCalls != 0 {
|
||||
t.Fatalf("enabled = %v, configure calls = %d, want saved preference without profile write", enabled, fake.configureCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetClaudeDesktopAutoModeAvoidsUnnecessaryRestart(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
if err := launch.SaveClaudeDesktopAutoMode(true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
previousAvailable := claudeAvailableModels
|
||||
claudeAvailableModels = mergeClaudeDesktopCloudInventory(
|
||||
proxy.ClaudeDesktopModelsFromRecommendations([]api.ModelRecommendation{{Model: "glm-5.2:cloud"}}),
|
||||
proxy.ClaudeDesktopModelsFromCloudInventory([]string{"glm-5.2:cloud"}),
|
||||
)
|
||||
previousDesktop := claudeDesktop
|
||||
previousRunning := claudeDesktopRunning
|
||||
fake := &fakeClaudeDesktopController{configured: true, profileCurrent: true, autoMode: true}
|
||||
claudeDesktop = fake
|
||||
claudeDesktopRunning = func() bool {
|
||||
t.Fatal("unchanged current preference should not inspect the Claude process")
|
||||
return false
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
claudeAvailableModels = previousAvailable
|
||||
claudeDesktop = previousDesktop
|
||||
claudeDesktopRunning = previousRunning
|
||||
})
|
||||
|
||||
if err := setClaudeDesktopAutoMode(true, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fake.configureCalls != 0 || fake.restart {
|
||||
t.Fatalf("unchanged preference triggered profile lifecycle: %+v", fake)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetClaudeDesktopAutoModeRewritesProfileBeforeRestart(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
previousDesktop := claudeDesktop
|
||||
previousRunning := claudeDesktopRunning
|
||||
fake := &fakeClaudeDesktopController{configured: true, profileCurrent: true, running: true}
|
||||
claudeDesktop = fake
|
||||
claudeDesktopRunning = func() bool { return true }
|
||||
t.Cleanup(func() {
|
||||
claudeDesktop = previousDesktop
|
||||
claudeDesktopRunning = previousRunning
|
||||
})
|
||||
|
||||
if err := setClaudeDesktopAutoMode(false, true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fake.configureCalls != 1 || !fake.profileCurrent || !fake.restart {
|
||||
t.Fatalf("profile restart lifecycle = %+v, want one profile write followed by restart", fake)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetClaudeDesktopAutoModeCancelDoesNotSavePreference(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
if err := launch.SaveClaudeDesktopAutoMode(true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
previousDesktop := claudeDesktop
|
||||
fake := &fakeClaudeDesktopController{
|
||||
configured: true, profileCurrent: true, running: true, autoMode: true,
|
||||
}
|
||||
claudeDesktop = fake
|
||||
t.Cleanup(func() { claudeDesktop = previousDesktop })
|
||||
|
||||
err := setClaudeDesktopAutoMode(false, false)
|
||||
if !errors.Is(err, launch.ErrClaudeDesktopRestartConfirmationRequired) {
|
||||
t.Fatalf("error = %v, want restart confirmation", err)
|
||||
}
|
||||
enabled, loadErr := launch.ClaudeDesktopAutoModeEnabled()
|
||||
if loadErr != nil {
|
||||
t.Fatal(loadErr)
|
||||
}
|
||||
if !enabled || fake.configureCalls != 0 || fake.restart {
|
||||
t.Fatalf("canceled Auto mode changed preference/profile: enabled=%v fake=%+v", enabled, fake)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetClaudeDesktopAutoModeKeepsDesiredPreferenceAfterProfileFailure(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
previousDesktop := claudeDesktop
|
||||
previousRunning := claudeDesktopRunning
|
||||
fake := &fakeClaudeDesktopController{configured: true, profileCurrent: true, configureErr: errors.New("profile write failed")}
|
||||
claudeDesktop = fake
|
||||
claudeDesktopRunning = func() bool { return false }
|
||||
t.Cleanup(func() {
|
||||
claudeDesktop = previousDesktop
|
||||
claudeDesktopRunning = previousRunning
|
||||
})
|
||||
|
||||
err := setClaudeDesktopAutoMode(false, true)
|
||||
if err == nil || !strings.Contains(err.Error(), "profile write failed") {
|
||||
t.Fatalf("setClaudeDesktopAutoMode error = %v, want profile write failure", err)
|
||||
}
|
||||
enabled, loadErr := launch.ClaudeDesktopAutoModeEnabled()
|
||||
if loadErr != nil {
|
||||
t.Fatal(loadErr)
|
||||
}
|
||||
if enabled {
|
||||
t.Fatal("desired preference should remain disabled so reconciliation can retry")
|
||||
}
|
||||
if fake.profileCurrent {
|
||||
t.Fatal("failed profile write should remain visibly out of date")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeDesktopAutoModeModelEligibility(t *testing.T) {
|
||||
recommended := proxy.ClaudeDesktopModelsFromRecommendations([]api.ModelRecommendation{
|
||||
{Model: "glm-5.2:cloud"},
|
||||
{Model: "kimi-k3:cloud"},
|
||||
{Model: "gemma4:31b-cloud"},
|
||||
})
|
||||
accountCloud := proxy.ClaudeDesktopModelsFromCloudInventory([]string{
|
||||
"glm-5.2:cloud",
|
||||
"gemma4:31b-cloud",
|
||||
})
|
||||
recommended = mergeClaudeDesktopCloudInventory(recommended, accountCloud)
|
||||
custom := proxy.SelectClaudeDesktopModels(nil, []string{"qwen3:8b"})
|
||||
tagOnly := proxy.SelectClaudeDesktopModels(nil, []string{"made-up:cloud"})
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
models []proxy.ClaudeDesktopModel
|
||||
want bool
|
||||
}{
|
||||
{name: "account cloud model", models: recommended[:1], want: true},
|
||||
{name: "recommendation absent from account list", models: recommended[1:2]},
|
||||
{name: "account gemma4 model", models: recommended[2:], want: true},
|
||||
{name: "custom model excluded", models: custom},
|
||||
{name: "cloud suffix alone excluded", models: tagOnly},
|
||||
{name: "account cloud and custom", models: []proxy.ClaudeDesktopModel{recommended[0], custom[0]}, want: true},
|
||||
{name: "empty selection excluded"},
|
||||
{name: "offline fallback excluded", models: fallbackClaudeDesktopModels()},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := claudeDesktopModelsSupportAutoMode(tt.models); got != tt.want {
|
||||
t.Fatalf("claudeDesktopModelsSupportAutoMode() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetClaudeDesktopAutoModeRejectsUnsupportedSelection(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
if err := launch.SaveClaudeDesktopAutoMode(false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
previousAvailable := claudeAvailableModels
|
||||
claudeAvailableModels = proxy.SelectClaudeDesktopModels(nil, []string{"qwen3:8b"})
|
||||
t.Cleanup(func() { claudeAvailableModels = previousAvailable })
|
||||
|
||||
err := setClaudeDesktopAutoMode(true, true)
|
||||
if err == nil || !strings.Contains(err.Error(), "cloud model available to your Ollama.com account") {
|
||||
t.Fatalf("setClaudeDesktopAutoMode() error = %v", err)
|
||||
}
|
||||
enabled, loadErr := launch.ClaudeDesktopAutoModeEnabled()
|
||||
if loadErr != nil {
|
||||
t.Fatal(loadErr)
|
||||
}
|
||||
if enabled {
|
||||
t.Fatal("rejected Auto mode change modified the saved preference")
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyClaudeDesktopMappingsPersistsSelection(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
|
||||
@@ -1343,9 +1144,6 @@ func TestApplyClaudeDesktopMappingsPersistsSelection(t *testing.T) {
|
||||
if !fake.installed {
|
||||
t.Fatal("expected the Claude profile to be installed")
|
||||
}
|
||||
if !fake.autoMode {
|
||||
t.Fatal("expected the account cloud model to keep Auto mode enabled")
|
||||
}
|
||||
if got, want := launch.ClaudeDesktopModels(), []string{"kimi-k3:cloud", "kimi-k3:cloud", "kimi-k3:cloud", "kimi-k3:cloud", "kimi-k3:cloud"}; !slices.Equal(got, want) {
|
||||
t.Fatalf("persisted models = %v, want Ollama routes %v", got, want)
|
||||
}
|
||||
@@ -1595,7 +1393,7 @@ func TestResetClaudeDesktopMappingsDoesNotOpenStoppedClaude(t *testing.T) {
|
||||
if err != nil || !applied {
|
||||
t.Fatalf("reset mappings = %v/%v, want persisted change", applied, err)
|
||||
}
|
||||
if !fake.configured || !fake.installed || fake.opened || fake.restart {
|
||||
if !fake.configured || !fake.installed || fake.opened || fake.restart || fake.configureCalls != 1 {
|
||||
t.Fatalf("stopped Claude reset = %+v, want configured without open or restart", fake)
|
||||
}
|
||||
if got := launch.ClaudeDesktopModelMappings(); !maps.Equal(got, defaultMappings) {
|
||||
|
||||
@@ -78,16 +78,6 @@ func bindClaudeDesktop(wv webview.WebView) {
|
||||
return result
|
||||
})
|
||||
|
||||
wv.Bind("setClaudeDesktopAutoMode", func(enabled, restartConfirmed bool) claudeDesktopActionResult {
|
||||
err := setClaudeDesktopAutoMode(enabled, restartConfirmed)
|
||||
result := claudeDesktopActionResult{Status: getClaudeDesktopConnectionStatus()}
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
result.RestartConfirmationRequired = errors.Is(err, launch.ErrClaudeDesktopRestartConfirmationRequired)
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
wv.Bind("getShowAppsInMenu", func() bool {
|
||||
return getShowAppsInMenu()
|
||||
})
|
||||
|
||||
@@ -24,7 +24,6 @@ type claudeDesktopStatus struct {
|
||||
GatewayPort int `json:"gatewayPort,omitempty"`
|
||||
RoutedRequests uint64 `json:"routedRequests"`
|
||||
Error string `json:"error,omitempty"`
|
||||
AutoMode bool `json:"autoMode"`
|
||||
ModelSource string `json:"modelSource,omitempty"`
|
||||
Models []claudeDesktopModelStatus `json:"models,omitempty"`
|
||||
Mappings []claudeDesktopMappingStatus `json:"mappings,omitempty"`
|
||||
@@ -42,7 +41,6 @@ type claudeDesktopModelStatus struct {
|
||||
Description string `json:"description,omitempty"`
|
||||
Cloud bool `json:"cloud"`
|
||||
Selected bool `json:"selected"`
|
||||
AutoMode bool `json:"autoMode"`
|
||||
Availability proxy.ClaudeDesktopAvailability `json:"availability"`
|
||||
Reason proxy.ClaudeDesktopAccessReason `json:"reason,omitempty"`
|
||||
RequiredPlan string `json:"requiredPlan,omitempty"`
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
type ReactNode,
|
||||
} from "react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { Switch } from "./ui/switch";
|
||||
import {
|
||||
ClaudeDesktopModelsSettings,
|
||||
type ClaudeDesktopModelsSettingsHandle,
|
||||
@@ -108,7 +107,6 @@ function testStatus(model = "glm-5.2:cloud", running = false) {
|
||||
running,
|
||||
startFailed: false,
|
||||
portConflict: false,
|
||||
autoMode: false,
|
||||
modelSource: "user" as const,
|
||||
mappings: [{ ...fableRoute, model }],
|
||||
models: [
|
||||
@@ -171,7 +169,7 @@ function textContent(node: ReactTestInstance): string {
|
||||
}
|
||||
|
||||
describe("ClaudeDesktopModelsSettings interactions", () => {
|
||||
it("opens below without scrolling and disables auto mode for draft changes", async () => {
|
||||
it("opens the model picker below without scrolling", async () => {
|
||||
class TestHTMLElement {
|
||||
focus() {}
|
||||
}
|
||||
@@ -201,7 +199,6 @@ describe("ClaudeDesktopModelsSettings interactions", () => {
|
||||
running: false,
|
||||
startFailed: false,
|
||||
portConflict: false,
|
||||
autoMode: true,
|
||||
modelSource: "user",
|
||||
mappings: [
|
||||
{
|
||||
@@ -216,14 +213,12 @@ describe("ClaudeDesktopModelsSettings interactions", () => {
|
||||
displayName: "glm-5.2:cloud",
|
||||
cloud: true,
|
||||
selected: true,
|
||||
autoMode: true,
|
||||
},
|
||||
{
|
||||
name: "kimi-k3:cloud",
|
||||
displayName: "kimi-k3:cloud",
|
||||
cloud: true,
|
||||
selected: false,
|
||||
autoMode: true,
|
||||
},
|
||||
],
|
||||
}}
|
||||
@@ -236,11 +231,6 @@ describe("ClaudeDesktopModelsSettings interactions", () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const autoModeSwitch = () =>
|
||||
renderer!.root.findByProps({ role: "switch" });
|
||||
expect(autoModeSwitch().props.disabled).not.toBe(true);
|
||||
expect(autoModeSwitch().props["aria-checked"]).toBe(true);
|
||||
|
||||
await act(async () => {
|
||||
pickerButton(renderer!).props.onClick();
|
||||
await Promise.resolve();
|
||||
@@ -260,20 +250,6 @@ describe("ClaudeDesktopModelsSettings interactions", () => {
|
||||
options[1].props.onClick();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(autoModeSwitch().props.disabled).toBe(true);
|
||||
expect(autoModeSwitch().props["aria-checked"]).toBe(true);
|
||||
expect(
|
||||
renderer!.root
|
||||
.findAllByType("p")
|
||||
.some((node) =>
|
||||
node.children
|
||||
.join("")
|
||||
.includes(
|
||||
"Start or restart Claude to apply model changes before changing auto mode.",
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
} finally {
|
||||
await act(async () => {
|
||||
renderer?.unmount();
|
||||
@@ -353,72 +329,6 @@ describe("ClaudeDesktopModelsSettings interactions", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("restores Auto mode when restart confirmation is canceled", async () => {
|
||||
class TestHTMLElement {
|
||||
focus() {}
|
||||
}
|
||||
const runningStatus = {
|
||||
...testStatus("glm-5.2:cloud", true),
|
||||
autoMode: true,
|
||||
models: testStatus().models.map((model) => ({
|
||||
...model,
|
||||
autoMode: true,
|
||||
})),
|
||||
};
|
||||
const setAutoMode = vi.fn().mockResolvedValue({
|
||||
status: runningStatus,
|
||||
error:
|
||||
"Claude Desktop restart confirmation is required before changing its profile",
|
||||
restartConfirmationRequired: true,
|
||||
});
|
||||
const confirm = vi.fn(() => false);
|
||||
vi.stubGlobal("window", {
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
HTMLElement: TestHTMLElement,
|
||||
setClaudeDesktopAutoMode: setAutoMode,
|
||||
confirm,
|
||||
});
|
||||
vi.stubGlobal("document", {
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
});
|
||||
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
||||
|
||||
let renderer: ReactTestRenderer | undefined;
|
||||
try {
|
||||
await act(async () => {
|
||||
renderer = create(
|
||||
<ClaudeDesktopModelsSettings
|
||||
initialLocalModels={[]}
|
||||
initialStatus={runningStatus}
|
||||
/>,
|
||||
);
|
||||
await Promise.resolve();
|
||||
});
|
||||
await act(async () => {
|
||||
renderer!.root.findByType(Switch).props.onChange(false);
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(setAutoMode).toHaveBeenCalledTimes(1);
|
||||
expect(setAutoMode).toHaveBeenCalledWith(false, false);
|
||||
expect(confirm).toHaveBeenCalledWith(
|
||||
"Restart Claude to change auto mode? Any running task will stop.",
|
||||
);
|
||||
expect(
|
||||
renderer!.root.findByProps({ role: "switch" }).props["aria-checked"],
|
||||
).toBe(true);
|
||||
} finally {
|
||||
await act(async () => {
|
||||
renderer?.unmount();
|
||||
await Promise.resolve();
|
||||
});
|
||||
vi.unstubAllGlobals();
|
||||
}
|
||||
});
|
||||
|
||||
it("ignores a stale focus refresh that finishes after apply", async () => {
|
||||
class TestHTMLElement {
|
||||
focus() {}
|
||||
|
||||
@@ -104,6 +104,8 @@ describe("ClaudeDesktopModelsSettings", () => {
|
||||
expect(html).not.toContain("routing");
|
||||
expect(html).not.toContain("Built-in defaults");
|
||||
expect(html).not.toContain("Unassigned");
|
||||
expect(html).not.toContain("Enable auto mode");
|
||||
expect(html).not.toContain('role="switch"');
|
||||
expect(html).toContain("Select a model");
|
||||
expect(html).toContain("Start Claude");
|
||||
});
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { getClaudeDesktopAvailableModels } from "@/api";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Description, Field, Label } from "@/components/ui/fieldset";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { claudeDesktopRecoveryMessage } from "@/lib/claudeDesktop";
|
||||
import { claudeDesktopModelStatusLabel } from "@/lib/claudeDesktopModelStatus";
|
||||
import type {
|
||||
@@ -35,7 +33,6 @@ export interface ClaudeDesktopModelsSettingsHandle {
|
||||
interface ClaudeDesktopModelsSettingsProps {
|
||||
initialStatus?: ClaudeDesktopStatus;
|
||||
initialLocalModels?: string[];
|
||||
initialCloudModels?: string[];
|
||||
includeCloudModels?: boolean;
|
||||
onDraftChange?: (hasChanges: boolean) => void;
|
||||
}
|
||||
@@ -123,12 +120,6 @@ function mappingRecord(
|
||||
);
|
||||
}
|
||||
|
||||
function formatModelList(names: string[]): string {
|
||||
if (names.length < 2) return names[0] ?? "";
|
||||
if (names.length === 2) return `${names[0]} or ${names[1]}`;
|
||||
return `${names.slice(0, -1).join(", ")}, or ${names[names.length - 1]}`;
|
||||
}
|
||||
|
||||
interface ClaudeModelPickerProps {
|
||||
id: string;
|
||||
routeName: string;
|
||||
@@ -263,7 +254,6 @@ export const ClaudeDesktopModelsSettings = forwardRef<
|
||||
{
|
||||
initialStatus,
|
||||
initialLocalModels,
|
||||
initialCloudModels,
|
||||
includeCloudModels = false,
|
||||
onDraftChange,
|
||||
},
|
||||
@@ -284,17 +274,10 @@ export const ClaudeDesktopModelsSettings = forwardRef<
|
||||
const [localModels, setLocalModels] = useState<string[]>(
|
||||
initialLocalModels ?? [],
|
||||
);
|
||||
const [accountCloudModels, setAccountCloudModels] = useState<string[]>(
|
||||
initialCloudModels ?? [],
|
||||
);
|
||||
const [modelsLoading, setModelsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [applying, setApplying] = useState(false);
|
||||
const [resettingMappings, setResettingMappings] = useState(false);
|
||||
const [autoModeApplying, setAutoModeApplying] = useState(false);
|
||||
const [autoModeOverride, setAutoModeOverride] = useState<boolean | null>(
|
||||
null,
|
||||
);
|
||||
const draftRef = useRef({ mappings, savedMappings });
|
||||
const statusRequestRef = useRef(0);
|
||||
const operationInFlightRef = useRef(false);
|
||||
@@ -353,11 +336,6 @@ export const ClaudeDesktopModelsSettings = forwardRef<
|
||||
.then((installed) => {
|
||||
if (!cancelled) {
|
||||
setLocalModels(installed.map((model) => model.model));
|
||||
setAccountCloudModels(
|
||||
installed
|
||||
.filter((model) => model.isCloud())
|
||||
.map((model) => model.model),
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -393,7 +371,7 @@ export const ClaudeDesktopModelsSettings = forwardRef<
|
||||
const model = catalogModels.find((candidate) => candidate.name === name);
|
||||
return !model || !modelIsAvailable(model);
|
||||
});
|
||||
const busy = applying || resettingMappings || autoModeApplying;
|
||||
const busy = applying || resettingMappings;
|
||||
|
||||
useEffect(() => {
|
||||
onDraftChange?.(hasDraftChanges);
|
||||
@@ -479,42 +457,6 @@ export const ClaudeDesktopModelsSettings = forwardRef<
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAutoMode = async (checked: boolean) => {
|
||||
if (!window.setClaudeDesktopAutoMode) {
|
||||
setError("Auto mode is available in the Ollama macOS app.");
|
||||
return;
|
||||
}
|
||||
setError(null);
|
||||
setAutoModeOverride(checked);
|
||||
setAutoModeApplying(true);
|
||||
operationInFlightRef.current = true;
|
||||
++statusRequestRef.current;
|
||||
try {
|
||||
let result = await window.setClaudeDesktopAutoMode(checked, false);
|
||||
if (result.restartConfirmationRequired) {
|
||||
applyStatus(result.status, true);
|
||||
if (
|
||||
!window.confirm(
|
||||
"Restart Claude to change auto mode? Any running task will stop.",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
result = await window.setClaudeDesktopAutoMode(checked, true);
|
||||
}
|
||||
++statusRequestRef.current;
|
||||
applyStatus(result.status);
|
||||
if (result.error) setError(result.error);
|
||||
} catch {
|
||||
setError("Ollama could not update Claude auto mode.");
|
||||
} finally {
|
||||
++statusRequestRef.current;
|
||||
operationInFlightRef.current = false;
|
||||
setAutoModeOverride(null);
|
||||
setAutoModeApplying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetToDefaults = useCallback(async (): Promise<boolean> => {
|
||||
if (operationInFlightRef.current) return false;
|
||||
|
||||
@@ -544,30 +486,6 @@ export const ClaudeDesktopModelsSettings = forwardRef<
|
||||
|
||||
if (!status?.supported || !status.used) return null;
|
||||
|
||||
const autoModeModelNames = Array.from(
|
||||
new Set([
|
||||
...models.filter((model) => model.autoMode).map((model) => model.name),
|
||||
...accountCloudModels,
|
||||
]),
|
||||
);
|
||||
const autoModeModelSet = new Set(autoModeModelNames);
|
||||
const autoModeAvailable =
|
||||
!hasDraftChanges &&
|
||||
assignedModels.length > 0 &&
|
||||
assignedModels.some((name) => autoModeModelSet.has(name));
|
||||
const autoMode = autoModeAvailable
|
||||
? (autoModeOverride ?? status.autoMode ?? false)
|
||||
: (status.autoMode ?? false);
|
||||
const autoModeDescription = hasDraftChanges
|
||||
? "Start or restart Claude to apply model changes before changing auto mode."
|
||||
: autoModeAvailable
|
||||
? "Let Claude decide when to ask before making changes."
|
||||
: accountCloudModels.length > 0
|
||||
? "Select a cloud model from Ollama.com to use auto mode."
|
||||
: autoModeModelNames.length > 0
|
||||
? `Select one of ${formatModelList(autoModeModelNames)} to use auto mode.`
|
||||
: "Auto mode needs a cloud model available to your Ollama.com account.";
|
||||
|
||||
const guidance =
|
||||
claudeDesktopRecoveryMessage(status.error, error) ??
|
||||
(hasDraftChanges && status.running
|
||||
@@ -663,21 +581,6 @@ export const ClaudeDesktopModelsSettings = forwardRef<
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Field className="mt-3 w-full max-w-xl border-t border-neutral-200 pt-3 dark:border-neutral-700">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="min-w-0">
|
||||
<Label>Enable auto mode</Label>
|
||||
<Description>{autoModeDescription}</Description>
|
||||
</div>
|
||||
<Switch
|
||||
checked={autoMode}
|
||||
disabled={busy || !autoModeAvailable}
|
||||
onChange={(checked) => void toggleAutoMode(checked)}
|
||||
className="flex-shrink-0"
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
|
||||
{guidance && (
|
||||
<p
|
||||
role={error || status.error ? "alert" : "status"}
|
||||
|
||||
Vendored
-6
@@ -24,7 +24,6 @@ interface ClaudeDesktopStatus {
|
||||
gatewayPort?: number;
|
||||
routedRequests?: number;
|
||||
error?: string;
|
||||
autoMode?: boolean;
|
||||
modelSource?: "user" | "endpoint" | "fallback";
|
||||
maxModels?: number;
|
||||
models?: ClaudeDesktopModelStatus[];
|
||||
@@ -43,7 +42,6 @@ interface ClaudeDesktopModelStatus {
|
||||
description?: string;
|
||||
cloud?: boolean;
|
||||
selected: boolean;
|
||||
autoMode?: boolean;
|
||||
availability?: "unknown" | "available" | "unavailable";
|
||||
reason?:
|
||||
| "cloud_off"
|
||||
@@ -95,10 +93,6 @@ declare global {
|
||||
resetClaudeDesktopMappings?: (
|
||||
restartConfirmed: boolean,
|
||||
) => Promise<ClaudeDesktopActionResult>;
|
||||
setClaudeDesktopAutoMode?: (
|
||||
enabled: boolean,
|
||||
restartConfirmed: boolean,
|
||||
) => Promise<ClaudeDesktopActionResult>;
|
||||
setOnboardingWindow?: (enabled: boolean) => void;
|
||||
menu: (items: MenuItem[]) => Promise<string | null>;
|
||||
OLLAMA_TOOLS?: boolean;
|
||||
|
||||
@@ -17,7 +17,6 @@ type integration struct {
|
||||
Models []string `json:"models"`
|
||||
Aliases map[string]string `json:"aliases,omitempty"`
|
||||
Onboarded bool `json:"onboarded,omitempty"`
|
||||
AutoMode *bool `json:"automode,omitempty"`
|
||||
}
|
||||
|
||||
// IntegrationConfig is the persisted config for one integration.
|
||||
@@ -144,45 +143,20 @@ func SaveIntegration(appName string, models []string) error {
|
||||
existing := cfg.Integrations[key]
|
||||
var aliases map[string]string
|
||||
var onboarded bool
|
||||
var autoMode *bool
|
||||
if existing != nil {
|
||||
aliases = existing.Aliases
|
||||
onboarded = existing.Onboarded
|
||||
autoMode = existing.AutoMode
|
||||
}
|
||||
|
||||
cfg.Integrations[key] = &integration{
|
||||
Models: models,
|
||||
Aliases: aliases,
|
||||
Onboarded: onboarded,
|
||||
AutoMode: autoMode,
|
||||
}
|
||||
|
||||
return save(cfg)
|
||||
}
|
||||
|
||||
// SaveIntegrationAutoMode saves an integration's auto mode preference while
|
||||
// preserving its models, aliases, and onboarding state.
|
||||
func SaveIntegrationAutoMode(appName string, enabled bool) error {
|
||||
if appName == "" {
|
||||
return errors.New("app name cannot be empty")
|
||||
}
|
||||
|
||||
cfg, err := load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
key := strings.ToLower(appName)
|
||||
existing := cfg.Integrations[key]
|
||||
if existing == nil {
|
||||
existing = &integration{}
|
||||
}
|
||||
existing.AutoMode = &enabled
|
||||
cfg.Integrations[key] = existing
|
||||
return save(cfg)
|
||||
}
|
||||
|
||||
// MarkIntegrationOnboarded marks an integration as onboarded in Ollama's config.
|
||||
func MarkIntegrationOnboarded(appName string) error {
|
||||
cfg, err := load()
|
||||
|
||||
+25
-57
@@ -225,66 +225,34 @@ func TestSaveIntegration_EmptyAppName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveIntegrationAutoMode(t *testing.T) {
|
||||
func TestSaveIntegrationDropsLegacyAutoMode(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
path := filepath.Join(tmpDir, ".ollama", "config.json")
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(`{"integrations":{"claude-desktop":{"models":["old"],"aliases":{"claude-sonnet-5":"old"},"onboarded":true,"automode":false}}}`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("save and load round-trip", func(t *testing.T) {
|
||||
if err := SaveIntegrationAutoMode("claude", true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config, err := LoadIntegration("claude")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if config.AutoMode == nil || !*config.AutoMode {
|
||||
t.Error("expected auto mode to be enabled")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("saveIntegration preserves auto mode", func(t *testing.T) {
|
||||
if err := SaveIntegration("claude", []string{"model-a"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config, err := LoadIntegration("claude")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if config.AutoMode == nil || !*config.AutoMode {
|
||||
t.Error("expected auto mode to survive a model save")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("auto mode preserves models", func(t *testing.T) {
|
||||
if err := SaveAliases("claude", map[string]string{"fast": "model-a"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := MarkIntegrationOnboarded("claude"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := SaveIntegrationAutoMode("claude", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config, err := LoadIntegration("claude")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if config.AutoMode == nil || *config.AutoMode {
|
||||
t.Error("expected auto mode to be disabled")
|
||||
}
|
||||
if len(config.Models) != 1 || config.Models[0] != "model-a" {
|
||||
t.Errorf("expected models to be preserved, got %v", config.Models)
|
||||
}
|
||||
if config.Aliases["fast"] != "model-a" || !config.Onboarded {
|
||||
t.Errorf("expected aliases and onboarding state to be preserved, got %+v", config)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty app name", func(t *testing.T) {
|
||||
if err := SaveIntegrationAutoMode("", true); err == nil {
|
||||
t.Error("expected error for empty app name, got nil")
|
||||
}
|
||||
})
|
||||
if err := SaveIntegration("claude-desktop", []string{"new"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(data), "automode") {
|
||||
t.Fatalf("legacy auto mode preference survived the next config save: %s", data)
|
||||
}
|
||||
cfg, err := LoadIntegration("claude-desktop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(cfg.Models) != 1 || cfg.Models[0] != "new" || cfg.Aliases["claude-sonnet-5"] != "old" || !cfg.Onboarded {
|
||||
t.Fatalf("integration settings were not preserved: %+v", cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadIntegration_NonexistentIntegration(t *testing.T) {
|
||||
|
||||
@@ -71,16 +71,6 @@ func (c *ClaudeDesktop) AutodiscoveredModel() string {
|
||||
// without pinning a model list, so Claude discovers the selected catalog and
|
||||
// exact Ollama route names the gateway advertises.
|
||||
func (c *ClaudeDesktop) ConfigureAutodiscovery() error {
|
||||
autoMode, err := claudeDesktopAutoModePreference()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.ConfigureAutodiscoveryWithAutoMode(autoMode)
|
||||
}
|
||||
|
||||
// ConfigureAutodiscoveryWithAutoMode writes the managed profile with the
|
||||
// effective Auto mode state selected by the Ollama app.
|
||||
func (c *ClaudeDesktop) ConfigureAutodiscoveryWithAutoMode(autoMode bool) error {
|
||||
if err := claudeDesktopSupported(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -91,7 +81,7 @@ func (c *ClaudeDesktop) ConfigureAutodiscoveryWithAutoMode(autoMode bool) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return configureClaudeDesktopTargets(targets, claudeDesktopGatewayBaseURL, "ollama", autoMode)
|
||||
return configureClaudeDesktopTargets(targets, claudeDesktopGatewayBaseURL, "ollama")
|
||||
}
|
||||
|
||||
func (c *ClaudeDesktop) RestoreHint() string {
|
||||
@@ -107,21 +97,11 @@ func (c *ClaudeDesktop) RestoreSuccessMessage() string {
|
||||
}
|
||||
|
||||
func (c *ClaudeDesktop) AutodiscoveryConfigured() bool {
|
||||
autoMode, err := claudeDesktopAutoModePreference()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return c.AutodiscoveryConfiguredWithAutoMode(autoMode)
|
||||
}
|
||||
|
||||
// AutodiscoveryConfiguredWithAutoMode reports whether the managed profile has
|
||||
// the effective Auto mode state selected by the Ollama app.
|
||||
func (c *ClaudeDesktop) AutodiscoveryConfiguredWithAutoMode(autoMode bool) bool {
|
||||
targets, err := claudeDesktopTargetPaths()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return claudeDesktopTargetsConfigured(targets, autoMode)
|
||||
return claudeDesktopTargetsConfigured(targets)
|
||||
}
|
||||
|
||||
// UsesOllamaGateway reports whether Claude Desktop is currently routed through
|
||||
@@ -137,27 +117,13 @@ func (c *ClaudeDesktop) UsesOllamaGateway() bool {
|
||||
|
||||
// SetInstalledFromDesktop changes the Claude profile from the native Ollama app.
|
||||
func (c *ClaudeDesktop) SetInstalledFromDesktop(installed, restart bool) error {
|
||||
autoMode := false
|
||||
if installed {
|
||||
var err error
|
||||
autoMode, err = claudeDesktopAutoModePreference()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return c.SetInstalledFromDesktopWithAutoMode(installed, restart, autoMode)
|
||||
}
|
||||
|
||||
// SetInstalledFromDesktopWithAutoMode changes the Claude profile from the
|
||||
// native Ollama app with its effective Auto mode state.
|
||||
func (c *ClaudeDesktop) SetInstalledFromDesktopWithAutoMode(installed, restart, autoMode bool) error {
|
||||
if err := claudeDesktopSupported(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
applyProfile := restoreClaudeDesktopProfile
|
||||
if installed {
|
||||
applyProfile = func() error { return c.ConfigureAutodiscoveryWithAutoMode(autoMode) }
|
||||
applyProfile = c.ConfigureAutodiscovery
|
||||
}
|
||||
|
||||
running, err := claudeDesktopIsRunning(context.Background())
|
||||
@@ -317,33 +283,6 @@ func RestoreClaudeDesktopModelMappings(models []string, mappings map[string]stri
|
||||
return config.SaveAliases(claudeDesktopIntegrationName, mappings)
|
||||
}
|
||||
|
||||
// ClaudeDesktopAutoModeEnabled reports the user's Claude Desktop auto mode
|
||||
// preference. It defaults to true when unset and returns configuration read
|
||||
// failures so callers do not mistake them for an explicit disabled setting.
|
||||
func ClaudeDesktopAutoModeEnabled() (bool, error) {
|
||||
return claudeDesktopAutoModePreference()
|
||||
}
|
||||
|
||||
// SaveClaudeDesktopAutoMode persists the user's Claude Desktop auto mode
|
||||
// preference in the shared launcher configuration.
|
||||
func SaveClaudeDesktopAutoMode(enabled bool) error {
|
||||
return config.SaveIntegrationAutoMode(claudeDesktopIntegrationName, enabled)
|
||||
}
|
||||
|
||||
func claudeDesktopAutoModePreference() (bool, error) {
|
||||
integrationConfig, err := config.LoadIntegration(claudeDesktopIntegrationName)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return true, nil
|
||||
}
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("load Claude Desktop auto mode preference: %w", err)
|
||||
}
|
||||
if integrationConfig.AutoMode == nil {
|
||||
return true, nil
|
||||
}
|
||||
return *integrationConfig.AutoMode, nil
|
||||
}
|
||||
|
||||
func (c *ClaudeDesktop) RequiresInteractiveOnboarding() bool {
|
||||
return false
|
||||
}
|
||||
@@ -382,9 +321,9 @@ func (c *ClaudeDesktop) Restore() error {
|
||||
})
|
||||
}
|
||||
|
||||
func configureClaudeDesktopTargets(targets claudeDesktopTargets, baseURL, apiKey string, autoMode bool) error {
|
||||
func configureClaudeDesktopTargets(targets claudeDesktopTargets, baseURL, apiKey string) error {
|
||||
for _, target := range targets.thirdPartyProfiles {
|
||||
if err := writeClaudeDesktopGatewayProfile(target.profile, baseURL, apiKey, true, autoMode); err != nil {
|
||||
if err := writeClaudeDesktopGatewayProfile(target.profile, baseURL, apiKey, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeClaudeDesktopMeta(target.meta, claudeDesktopProfileID, claudeDesktopProfileName); err != nil {
|
||||
@@ -738,7 +677,7 @@ func writeClaudeDesktopMeta(path, id, name string) error {
|
||||
return writeClaudeDesktopJSON(path, meta)
|
||||
}
|
||||
|
||||
func writeClaudeDesktopGatewayProfile(path, baseURL, apiKey string, forceChooser, autoMode bool) error {
|
||||
func writeClaudeDesktopGatewayProfile(path, baseURL, apiKey string, forceChooser bool) error {
|
||||
cfg, err := readClaudeDesktopJSONAllowMissing(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parse Claude Desktop Ollama profile: %w", err)
|
||||
@@ -754,7 +693,7 @@ func writeClaudeDesktopGatewayProfile(path, baseURL, apiKey string, forceChooser
|
||||
cfg["coworkEgressAllowedHosts"] = claudeDesktopEgressHosts
|
||||
cfg["disableEssentialTelemetry"] = true
|
||||
cfg["disableNonessentialTelemetry"] = true
|
||||
cfg["autoModeEnabled"] = autoMode
|
||||
cfg["autoModeEnabled"] = true
|
||||
return writeClaudeDesktopJSON(path, cfg)
|
||||
}
|
||||
|
||||
@@ -832,12 +771,12 @@ func readClaudeDesktopDeploymentMode(path string) string {
|
||||
return mode
|
||||
}
|
||||
|
||||
func claudeDesktopTargetsConfigured(targets claudeDesktopTargets, autoMode bool) bool {
|
||||
func claudeDesktopTargetsConfigured(targets claudeDesktopTargets) bool {
|
||||
if !claudeDesktopTargetsUseOllamaGateway(targets) {
|
||||
return false
|
||||
}
|
||||
for _, target := range targets.thirdPartyProfiles {
|
||||
if !claudeDesktopThirdPartyProfileConfigured(target, autoMode) {
|
||||
if !claudeDesktopThirdPartyProfileConfigured(target) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -864,7 +803,7 @@ func claudeDesktopTargetsUseOllamaGateway(targets claudeDesktopTargets) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func claudeDesktopThirdPartyProfileConfigured(target claudeDesktopThirdPartyPaths, autoMode bool) bool {
|
||||
func claudeDesktopThirdPartyProfileConfigured(target claudeDesktopThirdPartyPaths) bool {
|
||||
if !claudeDesktopThirdPartyProfileUsesOllamaGateway(target) {
|
||||
return false
|
||||
}
|
||||
@@ -894,7 +833,7 @@ func claudeDesktopThirdPartyProfileConfigured(target claudeDesktopThirdPartyPath
|
||||
if disabled, _ := cfg["disableNonessentialTelemetry"].(bool); !disabled {
|
||||
return false
|
||||
}
|
||||
if enabled, ok := cfg["autoModeEnabled"].(bool); !ok || enabled != autoMode {
|
||||
if enabled, _ := cfg["autoModeEnabled"].(bool); !enabled {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -14,8 +14,6 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ollama/ollama/cmd/config"
|
||||
)
|
||||
|
||||
func withClaudeDesktopPlatform(t *testing.T, goos string) {
|
||||
@@ -384,132 +382,57 @@ func TestClaudeDesktopConfigureAutodiscoveryRemovesExistingModelCatalog(t *testi
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeDesktopConfigureWritesSavedAutoModePreference(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
withClaudeDesktopPlatform(t, "darwin")
|
||||
t.Setenv("OLLAMA_API_KEY", "test-api-key")
|
||||
|
||||
paths, err := claudeDesktopConfigPaths()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
func TestClaudeDesktopConfigureAlwaysEnablesAutoMode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
legacyConfig string
|
||||
}{
|
||||
{name: "fresh setup"},
|
||||
{name: "legacy enabled", legacyConfig: `{"integrations":{"claude-desktop":{"models":["model-a"],"automode":true}}}`},
|
||||
{name: "legacy disabled", legacyConfig: `{"integrations":{"claude-desktop":{"models":["model-a"],"automode":false}}}`},
|
||||
{name: "legacy preference absent", legacyConfig: `{"integrations":{"claude-desktop":{"models":["model-a"]}}}`},
|
||||
}
|
||||
|
||||
if enabled, err := ClaudeDesktopAutoModeEnabled(); err != nil || !enabled {
|
||||
t.Fatal("auto mode should default to enabled")
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
withClaudeDesktopPlatform(t, "darwin")
|
||||
|
||||
if err := SaveClaudeDesktopAutoMode(true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := (&ClaudeDesktop{}).ConfigureAutodiscovery(); err != nil {
|
||||
t.Fatalf("ConfigureAutodiscovery returned error: %v", err)
|
||||
}
|
||||
profile := claudeDesktopReadJSON(t, paths.profile)
|
||||
if profile["autoModeEnabled"] != true {
|
||||
t.Fatalf("autoModeEnabled = %v, want true", profile["autoModeEnabled"])
|
||||
}
|
||||
var original []byte
|
||||
if tt.legacyConfig != "" {
|
||||
original = []byte(tt.legacyConfig)
|
||||
configPath := filepath.Join(tmpDir, ".ollama", "config.json")
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(configPath, original, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := SaveClaudeDesktopAutoMode(false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := (&ClaudeDesktop{}).ConfigureAutodiscovery(); err != nil {
|
||||
t.Fatalf("ConfigureAutodiscovery returned error: %v", err)
|
||||
}
|
||||
profile = claudeDesktopReadJSON(t, paths.profile)
|
||||
if profile["autoModeEnabled"] != false {
|
||||
t.Fatalf("autoModeEnabled = %v, want false", profile["autoModeEnabled"])
|
||||
}
|
||||
}
|
||||
if err := (&ClaudeDesktop{}).ConfigureAutodiscovery(); err != nil {
|
||||
t.Fatalf("ConfigureAutodiscovery returned error: %v", err)
|
||||
}
|
||||
paths, err := claudeDesktopConfigPaths()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
profile := claudeDesktopReadJSON(t, paths.profile)
|
||||
if profile["autoModeEnabled"] != true {
|
||||
t.Fatalf("autoModeEnabled = %v, want true", profile["autoModeEnabled"])
|
||||
}
|
||||
|
||||
func TestClaudeDesktopAutoModeDefaultsEnabledForLegacyIntegrationConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
|
||||
if err := config.SaveIntegration(claudeDesktopIntegrationName, []string{"model-a"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
enabled, err := ClaudeDesktopAutoModeEnabled()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !enabled {
|
||||
t.Fatal("auto mode should default to enabled when a legacy integration has no saved preference")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeDesktopEffectiveAutoModeDoesNotChangeSavedPreference(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
withClaudeDesktopPlatform(t, "darwin")
|
||||
t.Setenv("OLLAMA_API_KEY", "test-api-key")
|
||||
|
||||
if err := SaveClaudeDesktopAutoMode(true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c := &ClaudeDesktop{}
|
||||
if err := c.ConfigureAutodiscoveryWithAutoMode(false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
paths, err := claudeDesktopConfigPaths()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
profile := claudeDesktopReadJSON(t, paths.profile)
|
||||
if profile["autoModeEnabled"] != false {
|
||||
t.Fatalf("autoModeEnabled = %v, want effective false", profile["autoModeEnabled"])
|
||||
}
|
||||
if !c.AutodiscoveryConfiguredWithAutoMode(false) {
|
||||
t.Fatal("expected profile to match the effective Auto mode state")
|
||||
}
|
||||
if c.AutodiscoveryConfigured() {
|
||||
t.Fatal("saved preference check must detect the temporary effective state")
|
||||
}
|
||||
enabled, err := ClaudeDesktopAutoModeEnabled()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !enabled {
|
||||
t.Fatal("effective Auto mode state changed the saved preference")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeDesktopConfigurePreservesProfileWhenAutoModePreferenceIsUnreadable(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
withClaudeDesktopPlatform(t, "darwin")
|
||||
|
||||
paths, err := claudeDesktopConfigPaths()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(paths.profile), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
original := []byte(`{"autoModeEnabled":true,"userOwned":"keep"}`)
|
||||
if err := os.WriteFile(paths.profile, original, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
configPath := filepath.Join(tmpDir, ".ollama", "config.json")
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(configPath, []byte(`{invalid`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = (&ClaudeDesktop{}).ConfigureAutodiscovery()
|
||||
if err == nil || !strings.Contains(err.Error(), "load Claude Desktop auto mode preference") {
|
||||
t.Fatalf("ConfigureAutodiscovery error = %v, want unreadable preference error", err)
|
||||
}
|
||||
got, readErr := os.ReadFile(paths.profile)
|
||||
if readErr != nil {
|
||||
t.Fatal(readErr)
|
||||
}
|
||||
if !slices.Equal(got, original) {
|
||||
t.Fatalf("profile changed after preference read failure:\ngot: %s\nwant: %s", got, original)
|
||||
if tt.legacyConfig != "" {
|
||||
got, err := os.ReadFile(filepath.Join(tmpDir, ".ollama", "config.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !slices.Equal(got, original) {
|
||||
t.Fatalf("configuration changed during profile migration:\ngot: %s\nwant: %s", got, original)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -864,15 +787,12 @@ func TestClaudeDesktopAutodiscoveryConfiguredRequiresTelemetryDisabled(t *testin
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeDesktopAutodiscoveryConfiguredRequiresSavedAutoModePreference(t *testing.T) {
|
||||
func TestClaudeDesktopAutodiscoveryConfiguredRequiresAutoMode(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
withClaudeDesktopPlatform(t, "darwin")
|
||||
|
||||
c := &ClaudeDesktop{}
|
||||
if err := SaveClaudeDesktopAutoMode(true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.ConfigureAutodiscovery(); err != nil {
|
||||
t.Fatalf("Configure returned error: %v", err)
|
||||
}
|
||||
@@ -888,7 +808,7 @@ func TestClaudeDesktopAutodiscoveryConfiguredRequiresSavedAutoModePreference(t *
|
||||
}
|
||||
|
||||
if c.AutodiscoveryConfigured() {
|
||||
t.Fatal("expected Auto mode preference drift to force Claude Desktop profile repair")
|
||||
t.Fatal("expected disabled Auto mode to force Claude Desktop profile repair")
|
||||
}
|
||||
if !c.UsesOllamaGateway() {
|
||||
t.Fatal("expected Auto mode drift to leave Ollama routing active")
|
||||
@@ -1390,9 +1310,17 @@ func TestClaudeDesktopSetInstalledFromDesktopOmitsInferenceModelsWithoutMappedID
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeDesktopSetInstalledFromDesktopOpensStoppedAppWhenEnabled(t *testing.T) {
|
||||
setTestHome(t, t.TempDir())
|
||||
func TestClaudeDesktopReconnectAlwaysEnablesAutoMode(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
setTestHome(t, tmpDir)
|
||||
withClaudeDesktopPlatform(t, "darwin")
|
||||
configPath := filepath.Join(tmpDir, ".ollama", "config.json")
|
||||
if err := os.MkdirAll(filepath.Dir(configPath), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(configPath, []byte(`{"integrations":{"claude-desktop":{"automode":false}}}`), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
openCalls := 0
|
||||
withClaudeDesktopProcessHooks(t,
|
||||
func() bool { return false },
|
||||
@@ -1407,6 +1335,14 @@ func TestClaudeDesktopSetInstalledFromDesktopOpensStoppedAppWhenEnabled(t *testi
|
||||
if openCalls != 1 || !c.AutodiscoveryConfigured() {
|
||||
t.Fatalf("open calls/configured = %d/%v, want 1/true", openCalls, c.AutodiscoveryConfigured())
|
||||
}
|
||||
paths, err := claudeDesktopConfigPaths()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
profile := claudeDesktopReadJSON(t, paths.profile)
|
||||
if profile["autoModeEnabled"] != true {
|
||||
t.Fatalf("autoModeEnabled after reconnect = %v, want true", profile["autoModeEnabled"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestClaudeDesktopSetInstalledFromDesktopDoesNotOpenStoppedAppWhenDisabled(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user