diff --git a/docs/ai-coder/agents/models.md b/docs/ai-coder/agents/models.md
index 1f59c29ad3b..f8208d21eec 100644
--- a/docs/ai-coder/agents/models.md
+++ b/docs/ai-coder/agents/models.md
@@ -206,11 +206,11 @@ To change the default model:
The Models list reflects whether each model can actually be used:
- When a model's connected provider has been deleted, the **Provider** column
- shows **Unset** with an info tooltip that reads "The provider connected to
- this model has been deleted."
-- When a model's provider is missing or disabled, the **Status** column
- shows **Disabled**, regardless of the model's own enabled setting. Such a
- model cannot serve chat requests.
+ shows **Unset**.
+- When a model's provider is missing or disabled, an **Unavailable** badge
+ appears beside the model name. The badge's tooltip explains whether the
+ provider was deleted or disabled. Such a model cannot serve chat requests.
+- When a model is disabled, a **Disabled** badge appears beside the model name.
To reconnect a model to a working provider, open the model from the list,
pick a new provider from the **Provider** dropdown, and click **Save**. The
diff --git a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx
index 92277ce1299..e36fa352611 100644
--- a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx
+++ b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx
@@ -12,6 +12,7 @@ import {
CommandItem,
CommandList,
} from "#/components/Command/Command";
+import { Label } from "#/components/Label/Label";
import {
Popover,
PopoverContent,
@@ -92,7 +93,7 @@ export const OrganizationAutocomplete: FC = ({
aria-required={required}
data-testid="organization-autocomplete"
className={cn(
- "w-full justify-start gap-2 font-normal",
+ "group w-full justify-start gap-2 font-normal",
triggerClassName,
)}
>
@@ -154,3 +155,129 @@ export const OrganizationAutocomplete: FC = ({
);
};
+
+type OrganizationValueProps = {
+ organization: Organization;
+ labelOrganizations?: readonly Organization[];
+ id?: string;
+ className?: string;
+};
+
+const OrganizationValue: FC = ({
+ organization,
+ labelOrganizations,
+ id,
+ className,
+}) => {
+ const label = getOrganizationLabel(
+ organization,
+ labelOrganizations ?? [organization],
+ );
+ return (
+
)}
+ {addOrganizationField}
diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormFields.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormFields.tsx
index 2d8aff1304a..5301b6672ec 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormFields.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormFields.tsx
@@ -1,7 +1,12 @@
import type { FormikContextType } from "formik";
import { ChevronDownIcon, ChevronRightIcon, InfoIcon } from "lucide-react";
import type { FC, ReactNode } from "react";
-import { Link as RouterLink } from "react-router";
+import {
+ Link as RouterLink,
+ useLocation,
+ useNavigate,
+ useSearchParams,
+} from "react-router";
import { getVisibleProviderFields } from "#/api/chatModelOptions";
import type * as TypesGen from "#/api/typesGenerated";
import { Button } from "#/components/Button/Button";
@@ -19,6 +24,7 @@ import {
} from "#/components/InputGroup/InputGroup";
import { Label } from "#/components/Label/Label";
import { Link } from "#/components/Link/Link";
+import { OrganizationField } from "#/components/OrganizationAutocomplete/OrganizationAutocomplete";
import { Spinner } from "#/components/Spinner/Spinner";
import {
Tooltip,
@@ -40,7 +46,12 @@ import type {
import { cn } from "#/utils/cn";
import { docs } from "#/utils/docs";
import type { FormHelpers } from "#/utils/formUtils";
-import { useOrganizationModelsPath } from "../organizationModels";
+import {
+ creatableModelOrganizations,
+ selectModelOrganizationPath,
+ useOrganizationModels,
+ useOrganizationModelsPath,
+} from "../organizationModels";
import { ModelFormProviderSelect } from "./ModelFormProviderSelect";
const CollapsibleSection: FC<{
@@ -140,6 +151,41 @@ export const ModelFormFields: FC<{
setShowAdvanced,
}) => {
const modelsPath = useOrganizationModelsPath();
+ const { organization, accessibleOrganizations, permissionsByOrganization } =
+ useOrganizationModels();
+ const location = useLocation();
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+ const selectableOrganizations = isEditing
+ ? accessibleOrganizations
+ : creatableModelOrganizations(
+ accessibleOrganizations,
+ permissionsByOrganization,
+ );
+ const organizationField = isEditing ? (
+
+ ) : selectableOrganizations.length > 1 ? (
+ {
+ void navigate(
+ selectModelOrganizationPath(
+ location.pathname,
+ nextOrganization,
+ searchParams,
+ ),
+ );
+ }}
+ />
+ ) : null;
const hasProviderConfigFields =
getVisibleProviderFields(selectedProviderState.provider).length > 0;
@@ -249,6 +295,7 @@ export const ModelFormFields: FC<{
+ {organizationField}
diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderConfig.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderConfig.stories.tsx
index 13572da3df6..c424aa40f44 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderConfig.stories.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelFormProviderConfig.stories.tsx
@@ -1,17 +1,35 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import { expect, fn, userEvent, within } from "storybook/test";
import { reactRouterParameters } from "storybook-addon-remix-react-router";
+import {
+ MockDefaultOrganization,
+ MockOrganizationPermissions,
+} from "#/testHelpers/entities";
import { withToaster } from "#/testHelpers/storybook";
+import { OrganizationModelsContext } from "../organizationModels";
import {
MockAnthropicProviderState,
MockOpenAIProviderState,
} from "../testFixtures";
import { ModelForm } from "./ModelForm";
+const withOrganizationModels = (Story: React.FC) => (
+
+
+
+);
+
const meta: Meta = {
title: "pages/AISettingsPage/ModelsPage/ModelForm",
component: ModelForm,
- decorators: [withToaster],
+ decorators: [withToaster, withOrganizationModels],
args: {
providerStates: [MockOpenAIProviderState, MockAnthropicProviderState],
selectedProviderState: MockOpenAIProviderState,
diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.stories.tsx
index 01a39ce6508..9aa149a8dd8 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.stories.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.stories.tsx
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
-import { expect, userEvent, within } from "storybook/test";
+import { expect, fn, userEvent, within } from "storybook/test";
import { Table, TableBody } from "#/components/Table/Table";
import { mockClaude, mockGPT5 } from "../testFixtures";
import { ModelRow } from "./ModelRow";
@@ -32,49 +32,48 @@ const meta: Meta = {
export default meta;
type Story = StoryObj;
-// Control case for the effective-status logic: when both `hasProvider` and
-// `providerEnabled` are true, the status badge must reflect the persisted
-// enabled flag as-is. Any regression that inverts this collapses every model
-// to "Disabled" in the list.
+// Control case: a healthy, enabled model renders no status badge at all.
export const WithProvider: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("OpenAI")).toBeInTheDocument();
- await expect(canvas.getByText("Enabled")).toBeInTheDocument();
+ expect(canvas.queryByText("Enabled")).not.toBeInTheDocument();
+ expect(canvas.queryByText("Disabled")).not.toBeInTheDocument();
+ expect(canvas.queryByText("Unavailable")).not.toBeInTheDocument();
await expect(canvas.queryByText("Unset")).not.toBeInTheDocument();
},
};
-// When the provider is missing (soft-deleted or otherwise unavailable) the
-// Provider column shows "Unset" and the status collapses to "Disabled" even
-// though the persisted model.enabled flag is true. An info icon next to the
-// label reveals a tooltip explaining that the connected provider has been
-// deleted.
+// A missing (soft-deleted) provider shows "Unset" plus the "Unavailable"
+// notice even though the persisted model.enabled flag is true.
export const WithoutProviderForcesDisabled: Story = {
args: {
model: { ...mockClaude, enabled: true },
providerLabel: "",
hasProvider: false,
providerEnabled: false,
+ onClick: fn(),
},
- play: async ({ canvasElement }) => {
+ play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("Unset")).toBeInTheDocument();
- await expect(canvas.getByText("Disabled")).toBeInTheDocument();
- await expect(canvas.queryByText("Enabled")).not.toBeInTheDocument();
+ expect(canvas.queryByText("Disabled")).not.toBeInTheDocument();
- const info = canvas.getByLabelText("Provider status");
- await userEvent.hover(info);
+ // The badge is keyboard-focusable and must open its tooltip without
+ // activating the clickable row.
+ const notice = canvas.getByRole("button", { name: "Unavailable" });
+ notice.focus();
const tooltip = await within(document.body).findByRole("tooltip");
await expect(tooltip).toHaveTextContent(
"The provider connected to this model has been deleted.",
);
+ await userEvent.keyboard("{Enter}");
+ expect(args.onClick).not.toHaveBeenCalled();
},
};
-// When the provider exists but is disabled, the label still renders (the
-// provider is set) but the status collapses to "Disabled" because the model
-// is not usable.
+// A disabled provider keeps its label but the model shows the "Unavailable"
+// notice because it is not usable.
export const DisabledProviderForcesDisabled: Story = {
args: {
model: { ...mockClaude, enabled: true, is_default: false },
@@ -85,15 +84,21 @@ export const DisabledProviderForcesDisabled: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("Anthropic")).toBeInTheDocument();
- await expect(canvas.getByText("Disabled")).toBeInTheDocument();
- await expect(canvas.queryByText("Enabled")).not.toBeInTheDocument();
+ const notice = canvas.getByRole("button", { name: "Unavailable" });
+ await expect(notice).toBeInTheDocument();
+ expect(canvas.queryByText("Disabled")).not.toBeInTheDocument();
await expect(canvas.queryByText("Unset")).not.toBeInTheDocument();
+
+ await userEvent.hover(notice);
+ const tooltip = await within(document.body).findByRole("tooltip");
+ await expect(tooltip).toHaveTextContent(
+ "The provider connected to this model is disabled.",
+ );
},
};
-// A disabled model with an enabled provider keeps its provider label but
-// stays "Disabled". This exercises the enabled=false path so the "Unset"
-// wording is only tied to the missing provider case.
+// enabled=false with a healthy provider: "Disabled" badge beside the name,
+// no "Unavailable" notice, and no "Unset" wording.
export const DisabledModelWithProvider: Story = {
args: {
model: { ...mockClaude, enabled: false, is_default: false },
@@ -104,7 +109,9 @@ export const DisabledModelWithProvider: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("Anthropic")).toBeInTheDocument();
- await expect(canvas.getByText("Disabled")).toBeInTheDocument();
+ const nameCell = canvas.getByRole("cell", { name: /Claude Sonnet 4.5/ });
+ await expect(within(nameCell).getByText("Disabled")).toBeInTheDocument();
+ expect(canvas.queryByText("Unavailable")).not.toBeInTheDocument();
await expect(canvas.queryByText("Unset")).not.toBeInTheDocument();
},
};
diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx
index ee1e7e81e14..25b6e1432db 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx
@@ -1,4 +1,4 @@
-import { ChevronRightIcon, InfoIcon } from "lucide-react";
+import { ChevronRightIcon } from "lucide-react";
import type { FC } from "react";
import type { ChatModel } from "#/api/typesGenerated";
import { Avatar } from "#/components/Avatar/Avatar";
@@ -11,6 +11,7 @@ import {
} from "#/components/Tooltip/Tooltip";
import { useClickableTableRow } from "#/hooks/useClickableTableRow";
import { ProviderIcon } from "#/pages/AISettingsPage/ProvidersPage/components/ProviderIcon";
+import { cn } from "#/utils/cn";
type ModelRowProps = {
model: ChatModel;
@@ -39,8 +40,17 @@ export const ModelRow: FC = ({
const clickableProps = useClickableTableRow({ onClick });
const displayName = model.display_name || model.model;
// Models whose provider is missing or disabled cannot be used, so the
- // status column reflects that regardless of the persisted enabled flag.
- const isEffectivelyEnabled = model.enabled && hasProvider && providerEnabled;
+ // status cell surfaces that regardless of the persisted enabled flag.
+ const providerNotice = !hasProvider
+ ? "The provider connected to this model has been deleted."
+ : !providerEnabled
+ ? "The provider connected to this model is disabled."
+ : null;
+
+ // Keep tooltip activation from triggering the clickable row's navigation.
+ const stopPropagation = (event: React.SyntheticEvent) => {
+ event.stopPropagation();
+ };
return (
@@ -48,7 +58,10 @@ export const ModelRow: FC = ({