Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions apps/app/src/react-app/domains/settings/pages/ai-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, DeviceProviderShareRow>;
onShareProvider?: (provider: { id: string; name: string }) => void;
/** Opens the Ollama page from the bottom of AI Providers. */
onUseOllama?: () => void;
};

function deviceSourceLabel(source: ConnectedProvider["source"]) {
Expand Down Expand Up @@ -241,5 +243,9 @@ export function AiSettingsView(props: AiSettingsViewProps) {
{props.gatewayConnectProviders?.map((provider) => <GatewayConnectRow key={gatewayConnectProviderKey(provider)} provider={provider} busy={props.connectingGatewayProviderId === gatewayConnectProviderKey(provider)} onConnect={props.onConnectGatewayProvider} onCancel={props.onCancelGatewayConnect} />)}
</ProviderList>
</section> : null)}

{props.onUseOllama ? <div>
<Button variant="outline" data-testid="ai-providers-use-ollama" onClick={props.onUseOllama}>Use Ollama</Button>
</div> : null}
</LayoutStack>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,8 @@ export function getGlobalSettingsTabs(
capabilities: Pick<PlatformCapabilities, "autoUpdate">,
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");
Expand All @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
SettingsSidebar,
getSettingsTabIcon,
getSettingsTabLabel,
isSettingsTabActive,
isSettingsTabBeta,
useSettingsNavGroups,
} from "./settings-page";
Expand Down Expand Up @@ -194,7 +195,7 @@ function SettingsSectionMenu(props: Pick<SettingsPageFrameProps, "activeTab" | "
<DropdownMenuItem
key={tab}
onClick={() => 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}
>
<Icon />
<span>{getSettingsTabLabel(tab)}</span>
Expand Down
3 changes: 3 additions & 0 deletions apps/app/src/react-app/shell/command-palette-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<SettingsTab, string[]>> = {
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"],
Expand Down Expand Up @@ -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 }));

Expand Down
3 changes: 3 additions & 0 deletions apps/app/src/react-app/shell/settings-route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2394,6 +2394,9 @@ function SettingsRouteContent(props: SettingsSurfaceProps = {}) {
showOpenWorkModelsDeviceRow={openWorkModelsRow.showDeviceRow}
organizationProviderIds={organizationProviderIds}
onOpenDen={openProviderCloudPage}
onUseOllama={!checkDesktopRestriction({ restriction: "allowLocalModels" })
? () => navigateSettingsPath("ollama")
: undefined}
cloudProvidersView={
<CloudProvidersView
runtimeWorkspaceId={runtimeWorkspaceId}
Expand Down
60 changes: 60 additions & 0 deletions apps/app/tests/ollama-in-ai-providers.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/** @jsxImportSource react */
import { afterAll, expect, test } from "bun:test";
import { GlobalRegistrator } from "@happy-dom/global-registrator";
import { renderToStaticMarkup } from "react-dom/server";

const ownedDom = typeof window === "undefined";
if (ownedDom) GlobalRegistrator.register({ url: "http://localhost/" });
const { getGlobalSettingsTabs, isSettingsTabActive } = await import("../src/react-app/domains/settings/shell/settings-page");
const { buildCommandPaletteSettingsItems } = await import("../src/react-app/shell/command-palette-settings");
const { AiSettingsView } = await import("../src/react-app/domains/settings/pages/ai-view");
type AiSettingsViewProps = import("../src/react-app/domains/settings/pages/ai-view").AiSettingsViewProps;

afterAll(async () => {
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(<AiSettingsView {...view} />)).not.toContain("Use Ollama");
const withButton = renderToStaticMarkup(<AiSettingsView {...view} onUseOllama={() => {}} />);
expect(withButton).toContain("ai-providers-use-ollama");
expect(withButton).toContain("Use Ollama");
});
Loading