app: always enable Claude auto mode

This commit is contained in:
ParthSareen
2026-09-03 17:07:48 -07:00
parent 59fe23d85c
commit 3e1cdf3828
12 changed files with 124 additions and 808 deletions
+9 -105
View File
@@ -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
+9 -211
View File
@@ -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()
})
-2
View File
@@ -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"}
-6
View File
@@ -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;
-26
View File
@@ -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
View File
@@ -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) {
+11 -72
View File
@@ -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
+66 -130
View File
@@ -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) {