diff --git a/apps/app/src/react-app/domains/settings/pages/ai-view.tsx b/apps/app/src/react-app/domains/settings/pages/ai-view.tsx index 76b6ca3e18..eb1095a5cb 100644 --- a/apps/app/src/react-app/domains/settings/pages/ai-view.tsx +++ b/apps/app/src/react-app/domains/settings/pages/ai-view.tsx @@ -77,6 +77,8 @@ export type AiSettingsViewProps = { /** Device provider sharing: which keys on this device can be shared with the organization, or already are. */ shareRows?: ReadonlyMap; onShareProvider?: (provider: { id: string; name: string }) => void; + /** Opens the Ollama page from the bottom of AI Providers. */ + onUseOllama?: () => void; }; function deviceSourceLabel(source: ConnectedProvider["source"]) { @@ -241,5 +243,9 @@ export function AiSettingsView(props: AiSettingsViewProps) { {props.gatewayConnectProviders?.map((provider) => )} : null)} + + {props.onUseOllama ?
+ +
: null} ; } diff --git a/apps/app/src/react-app/domains/settings/shell/settings-page.tsx b/apps/app/src/react-app/domains/settings/shell/settings-page.tsx index 4d93b15128..b99ba66092 100644 --- a/apps/app/src/react-app/domains/settings/shell/settings-page.tsx +++ b/apps/app/src/react-app/domains/settings/shell/settings-page.tsx @@ -222,7 +222,8 @@ export function getGlobalSettingsTabs( capabilities: Pick, options: { desktop?: boolean } = {}, ): SettingsTab[] { - const tabs: SettingsTab[] = ["ai", "ollama", "appearance", "shortcuts", "environment"]; + // Ollama opens from a button on AI Providers, not from the sidebar. + const tabs: SettingsTab[] = ["ai", "appearance", "shortcuts", "environment"]; if (options.desktop) tabs.push("computer"); if (capabilities.autoUpdate) tabs.push("updates"); if (developerMode) tabs.push("debug"); @@ -239,6 +240,8 @@ export function isSettingsTabBeta(_tab: SettingsTab) { } export function isSettingsTabActive(activeTab: SettingsTab, tab: SettingsTab) { + // The Ollama page is reached from AI Providers, which stays highlighted there. + if (activeTab === "ollama") return tab === "ai"; return activeTab === tab; } diff --git a/apps/app/src/react-app/domains/settings/shell/settings-shell.tsx b/apps/app/src/react-app/domains/settings/shell/settings-shell.tsx index d5fe457cd5..d49667bc84 100644 --- a/apps/app/src/react-app/domains/settings/shell/settings-shell.tsx +++ b/apps/app/src/react-app/domains/settings/shell/settings-shell.tsx @@ -27,6 +27,7 @@ import { SettingsSidebar, getSettingsTabIcon, getSettingsTabLabel, + isSettingsTabActive, isSettingsTabBeta, useSettingsNavGroups, } from "./settings-page"; @@ -194,7 +195,7 @@ function SettingsSectionMenu(props: Pick props.onSelectTab(tab)} - className={props.activeTab === tab ? "bg-foreground/10 text-accent-foreground" : undefined} + className={isSettingsTabActive(props.activeTab, tab) ? "bg-foreground/10 text-accent-foreground" : undefined} > {getSettingsTabLabel(tab)} diff --git a/apps/app/src/react-app/shell/command-palette-settings.ts b/apps/app/src/react-app/shell/command-palette-settings.ts index 34eb68bfaf..9f0d61605b 100644 --- a/apps/app/src/react-app/shell/command-palette-settings.ts +++ b/apps/app/src/react-app/shell/command-palette-settings.ts @@ -15,6 +15,7 @@ import { ADVANCED_SETTINGS_SECTIONS } from "@/react-app/domains/settings/advance import type { PaletteItem } from "./command-palette-search"; const SETTINGS_KEYWORDS: Partial> = { + ollama: ["local models", "llama", "offline", "on device"], ai: ["provider", "providers", "api key", "anthropic", "openai", "openrouter", "google", "gemini", "claude", "gpt", "connect model", "models"], preferences: ["default model", "reasoning", "thinking", "compaction", "effort"], permissions: ["authorized folders", "folder access", "file access", "allow", "permission denied", "sandbox", "approvals"], @@ -52,6 +53,8 @@ export function buildCommandPaletteSettingsItems(input: { "general", ...getWorkspaceSettingsTabs(), ...getGlobalSettingsTabs(input.developerMode, input.capabilities), + // Not in the sidebar (opened from AI Providers), but still searchable. + "ollama", ...CLOUD_SETTINGS_TABS, ] satisfies SettingsTab[]).filter((tab) => isSettingsTabAllowed({ tab, checkRestriction })); diff --git a/apps/app/src/react-app/shell/settings-route.tsx b/apps/app/src/react-app/shell/settings-route.tsx index 893b8a08e5..0b9334b77c 100644 --- a/apps/app/src/react-app/shell/settings-route.tsx +++ b/apps/app/src/react-app/shell/settings-route.tsx @@ -2394,6 +2394,9 @@ function SettingsRouteContent(props: SettingsSurfaceProps = {}) { showOpenWorkModelsDeviceRow={openWorkModelsRow.showDeviceRow} organizationProviderIds={organizationProviderIds} onOpenDen={openProviderCloudPage} + onUseOllama={!checkDesktopRestriction({ restriction: "allowLocalModels" }) + ? () => navigateSettingsPath("ollama") + : undefined} cloudProvidersView={ { + if (ownedDom) await GlobalRegistrator.unregister(); +}); + +test("the sidebar drops Ollama and highlights AI Providers while the Ollama page is open", () => { + const tabs = getGlobalSettingsTabs(false, { autoUpdate: false }); + expect(tabs).not.toContain("ollama"); + expect(tabs).toContain("ai"); + expect(isSettingsTabActive("ollama", "ai")).toBe(true); + expect(isSettingsTabActive("ollama", "ollama")).toBe(false); + expect(isSettingsTabActive("appearance", "ai")).toBe(false); +}); + +test("the command palette still finds the Ollama page", () => { + const items = buildCommandPaletteSettingsItems({ + developerMode: false, + capabilities: { autoUpdate: false }, + onOpenSettings: () => {}, + onOpenExtensions: () => {}, + }); + expect(items.some((item) => item.id === "settings:ollama")).toBe(true); +}); + +const view: AiSettingsViewProps = { + busy: false, + providerAuthBusy: false, + providerStatusLabel: "", + providerStatusStyle: "", + providerSummary: "", + providerLoadState: { status: "ready", error: null }, + onRetryProviders: () => {}, + connectedProviders: [], + disconnectingProviderId: null, + providerConnectError: null, + providerDisconnectStatus: null, + providerDisconnectError: null, + onOpenProviderAuth: () => {}, + onDisconnectProvider: () => {}, + canDisconnectProvider: () => true, + canAddProviders: true, +}; + +test("AI Providers shows Use Ollama only when the page is given a way to open it", () => { + expect(renderToStaticMarkup()).not.toContain("Use Ollama"); + const withButton = renderToStaticMarkup( {}} />); + expect(withButton).toContain("ai-providers-use-ollama"); + expect(withButton).toContain("Use Ollama"); +});