@@ -44,9 +49,6 @@ export const MCPServerRow: FC = ({ server, onClick }) => {
{AVAILABILITY_LABELS[server.availability] ?? server.availability}
-
- {enabled ? "Enabled" : "Disabled"}
-
{onClick && (
diff --git a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx
index a154452569d..06f3db7d7bc 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx
@@ -94,9 +94,14 @@ export const Default: Story = {
await expect(canvas.getByText("AWS Bedrock")).toBeInTheDocument();
// The provider icon is decorative (alt=""), so its name comes from the
// visible label asserted above rather than the image alt text.
- await expect(canvas.getAllByText("Enabled").length).toBeGreaterThan(0);
+ expect(canvas.queryByText("Enabled")).not.toBeInTheDocument();
await expect(canvas.getByText("Default")).toBeInTheDocument();
- await expect(canvas.getByText("Disabled")).toBeInTheDocument();
+ // The disabled badge renders beside the model name, not in a status
+ // column.
+ const disabledRow = canvas.getByRole("button", { name: /GPT-4o mini/i });
+ await expect(within(disabledRow).getByText("Disabled")).toBeInTheDocument();
+ const enabledRow = canvas.getByRole("button", { name: /^GPT-5/i });
+ expect(within(enabledRow).queryByText("Disabled")).not.toBeInTheDocument();
// The Add model menu lists each provider by exact accessible name; a
// regressed icon would turn a name into "Anthropic Anthropic".
@@ -184,11 +189,12 @@ export const DisabledProviderModelsStillListed: Story = {
// so the row is queried by its clickable role.
const row = canvas.getByRole("button", { name: /GPT-4o Secondary/i });
await expect(within(row).getByText("OpenAI Secondary")).toBeInTheDocument();
- // A model under a disabled provider is not usable, so the status
- // column must show "Disabled" even though the stored enabled flag is
- // true. Scope to the target row so a fixture change cannot pass this
- // assertion via an unrelated "Disabled" cell.
- await expect(within(row).getByText("Disabled")).toBeInTheDocument();
+ // A model under a disabled provider is not usable, so the status cell
+ // must show the unavailable notice even though the stored enabled flag
+ // is true. Scope to the target row so a fixture change cannot pass
+ // this assertion via an unrelated cell.
+ await expect(within(row).getByText("Unavailable")).toBeInTheDocument();
+ expect(within(row).queryByText("Disabled")).not.toBeInTheDocument();
},
};
@@ -197,8 +203,8 @@ export const DisabledProviderModelsStillListed: Story = {
// models entirely, so the row reaches "Unset" via a map-miss and the
// `?? false` fallback at ModelsPageView.tsx wiring. Reproduce that shape
// here: the model appears in `models` but is not present in any
-// providerState.models, so a `?? true` regression would flip this
-// story to "Enabled" and be caught.
+// providerState.models, so a `?? true` regression would hide the
+// unavailable notice and be caught.
export const OrphanedModelShowsUnset: Story = {
args: {
models: [mockGPT5, mockOrphanedModel],
@@ -209,7 +215,7 @@ export const OrphanedModelShowsUnset: Story = {
const canvas = within(canvasElement);
const row = canvas.getByRole("button", { name: /Orphaned Model/i });
await expect(within(row).getByText("Unset")).toBeInTheDocument();
- await expect(within(row).getByText("Disabled")).toBeInTheDocument();
+ await expect(within(row).getByText("Unavailable")).toBeInTheDocument();
},
};
diff --git a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx
index 499e3cbce55..2a6b661d688 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx
@@ -275,7 +275,9 @@ const ModelsPageView: FC = ({
NameProviderContext limit
- Status
+
+ Status
+ Open model
diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.stories.tsx
index 01a39ce6508..bfe39091d69 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.stories.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.stories.tsx
@@ -32,24 +32,25 @@ 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: with an enabled provider and an enabled model, the row
+// renders no status badge at all. Any regression that surfaces a badge for a
+// healthy model shows up here.
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.
+// Provider column shows "Unset" and the status cell shows an "Unavailable"
+// notice 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.
export const WithoutProviderForcesDisabled: Story = {
args: {
model: { ...mockClaude, enabled: true },
@@ -60,8 +61,8 @@ export const WithoutProviderForcesDisabled: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(canvas.getByText("Unset")).toBeInTheDocument();
- await expect(canvas.getByText("Disabled")).toBeInTheDocument();
- await expect(canvas.queryByText("Enabled")).not.toBeInTheDocument();
+ await expect(canvas.getByText("Unavailable")).toBeInTheDocument();
+ expect(canvas.queryByText("Disabled")).not.toBeInTheDocument();
const info = canvas.getByLabelText("Provider status");
await userEvent.hover(info);
@@ -73,8 +74,8 @@ export const WithoutProviderForcesDisabled: Story = {
};
// 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.
+// provider is set) but the status cell shows the "Unavailable" notice with a
+// tooltip because the model is not usable.
export const DisabledProviderForcesDisabled: Story = {
args: {
model: { ...mockClaude, enabled: true, is_default: false },
@@ -85,15 +86,23 @@ 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.getByText("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.
+// A disabled model with an enabled provider shows the "Disabled" badge next
+// to the name (like the "Default" badge) with no status-cell notice. This
+// exercises the enabled=false path so the "Unset" wording is only tied to
+// the missing provider case.
export const DisabledModelWithProvider: Story = {
args: {
model: { ...mockClaude, enabled: false, is_default: false },
@@ -104,7 +113,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..85a7a1c744d 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx
@@ -39,8 +39,12 @@ 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;
return (
@@ -66,6 +70,11 @@ export const ModelRow: FC = ({
Default
)}
+ {!model.enabled && (
+
+ Disabled
+
+ )}
@@ -102,9 +111,16 @@ export const ModelRow: FC = ({
-
- {isEffectivelyEnabled ? "Enabled" : "Disabled"}
-
+ {providerNotice && (
+
+
+ Unavailable
+
+
+ {providerNotice}
+
+
+ )}
From 76e16aefad98704eac83830f36e21663eebb1a70 Mon Sep 17 00:00:00 2001
From: Tracy Johnson
Date: Tue, 25 Aug 2026 15:47:32 +0000
Subject: [PATCH 03/19] feat(site): make the organization informational on the
model edit page
---
.../OrganizationAutocomplete.tsx | 42 +++++++++++++++++++
.../components/OrganizationPicker.tsx | 17 +++-----
.../UpdateModelPageView.stories.tsx | 13 ++++++
.../ModelsPage/components/ModelFormFields.tsx | 5 ++-
.../components/ModelOrganizationSelect.tsx | 17 ++++++--
5 files changed, 77 insertions(+), 17 deletions(-)
diff --git a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx
index 92277ce1299..ce7b5a443d8 100644
--- a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx
+++ b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx
@@ -154,3 +154,45 @@ export const OrganizationAutocomplete: FC = ({
);
};
+
+type OrganizationValueProps = {
+ organization: Organization;
+ // Collision set for disambiguation labels, matching the autocomplete's
+ // labelOrganizations behavior. Defaults to just the organization.
+ labelOrganizations?: readonly Organization[];
+ id?: string;
+ className?: string;
+};
+
+// Read-only counterpart to OrganizationAutocomplete for contexts where the
+// organization is informational and cannot be changed. Renders a static
+// value instead of a disabled control so the text keeps its normal color
+// and stays out of the tab order.
+export const OrganizationValue: FC = ({
+ organization,
+ labelOrganizations,
+ id,
+ className,
+}) => {
+ const label = getOrganizationLabel(
+ organization,
+ labelOrganizations ?? [organization],
+ );
+ return (
+
+ );
+};
diff --git a/site/src/pages/AISettingsPage/MCPServersPage/components/OrganizationPicker.tsx b/site/src/pages/AISettingsPage/MCPServersPage/components/OrganizationPicker.tsx
index 971d7297be8..8c7b9223cb4 100644
--- a/site/src/pages/AISettingsPage/MCPServersPage/components/OrganizationPicker.tsx
+++ b/site/src/pages/AISettingsPage/MCPServersPage/components/OrganizationPicker.tsx
@@ -1,10 +1,10 @@
import type { FC } from "react";
import type { Organization } from "#/api/typesGenerated";
-import { Avatar } from "#/components/Avatar/Avatar";
import { Label } from "#/components/Label/Label";
import {
getOrganizationLabel,
OrganizationAutocomplete,
+ OrganizationValue,
} from "#/components/OrganizationAutocomplete/OrganizationAutocomplete";
import { cn } from "#/utils/cn";
@@ -56,18 +56,11 @@ export const OrganizationPicker: FC = ({
-
-
-
- {
- if (onToggleEnabled) {
- onToggleEnabled(checked);
- }
- }}
- disabled={isDisabled}
- aria-disabled={lacksUpdatePermission}
- aria-describedby={
- lacksUpdatePermission ? disabledReasonId : undefined
- }
- aria-label="Server enabled"
- className="aria-disabled:cursor-not-allowed aria-disabled:data-[state=checked]:bg-surface-tertiary aria-disabled:data-[state=unchecked]:bg-surface-tertiary"
- />
-
-
-
- {lacksUpdatePermission
- ? "You do not have permission to update this server."
- : server.enabled
- ? "Disable this server. It will be hidden from agents."
- : "Enable this server. It will be visible to agents."}
-
-
- {lacksUpdatePermission && (
-
- You do not have permission to update this server.
-
+
- {showLabel && }
+ // The label and gap mirror the form Field primitive so the picker
+ // lines up with sibling fields when rendered inside a form grid.
+
+ {showLabel && (
+
+ )}
{isReadOnly ? (
Date: Tue, 25 Aug 2026 16:06:51 +0000
Subject: [PATCH 05/19] feat(site): dim disabled model and MCP server rows like
providers
---
.../components/MCPServerRow.tsx | 12 ++++++---
.../ModelsPage/components/ModelRow.tsx | 27 ++++++++++++++++---
2 files changed, 31 insertions(+), 8 deletions(-)
diff --git a/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerRow.tsx b/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerRow.tsx
index abfa20d48b7..d73846c37d9 100644
--- a/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerRow.tsx
+++ b/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerRow.tsx
@@ -26,12 +26,12 @@ export const MCPServerRow: FC = ({ server, onClick }) => {
{server.display_name}
@@ -43,10 +43,14 @@ export const MCPServerRow: FC = ({ server, onClick }) => {
)}
-
+
{AUTH_TYPE_LABELS[server.auth_type] ?? server.auth_type}
-
+
{AVAILABILITY_LABELS[server.availability] ?? server.availability}
diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx
index 85a7a1c744d..5cdd5d888a2 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx
@@ -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;
@@ -52,7 +53,10 @@ export const ModelRow: FC = ({
= ({
{displayName}
@@ -81,7 +90,12 @@ export const ModelRow: FC = ({
{hasProvider ? (
{providerLabel}
@@ -106,7 +120,12 @@ export const ModelRow: FC = ({
)}
-
+
{formatContextLimit(model.context_limit)}
From 22670372cb78575bd211612d6c6a6866e6cfb941 Mon Sep 17 00:00:00 2001
From: Tracy Johnson
Date: Tue, 25 Aug 2026 16:25:58 +0000
Subject: [PATCH 06/19] feat(site): move the model unavailable notice into the
name cell
---
.../ModelsPage/ModelsPageView.tsx | 3 --
.../ModelsPage/components/ModelRow.tsx | 39 +++++++++++++------
2 files changed, 27 insertions(+), 15 deletions(-)
diff --git a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx
index 2a6b661d688..46e8c6724b2 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.tsx
@@ -275,9 +275,6 @@ const ModelsPageView: FC = ({
NameProviderContext limit
-
- Status
- Open model
diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx
index 5cdd5d888a2..75279869174 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx
@@ -47,6 +47,13 @@ export const ModelRow: FC = ({
? "The provider connected to this model is disabled."
: null;
+ // Stop activation from bubbling to the clickable row, which navigates on
+ // click, Enter (onKeyDown), and Space (onKeyUp). Radix composes its own
+ // click handler, so the tooltip still opens.
+ const stopPropagation = (event: React.SyntheticEvent) => {
+ event.stopPropagation();
+ };
+
return (
@@ -84,6 +91,26 @@ export const ModelRow: FC = ({
Disabled
)}
+ {providerNotice && (
+
+
+
+ Unavailable
+
+
+
+ {providerNotice}
+
+
+ )}
Date: Tue, 25 Aug 2026 16:24:55 +0000
Subject: [PATCH 07/19] feat(site/src/pages/AISettingsPage/MCPServersPage):
disable update submit until the edit form is dirty
---
.../MCPServersPage/MCPServersPage.stories.tsx | 5 +++++
.../UpdateMCPServerPage/UpdateMCPServerPageView.stories.tsx | 6 +++++-
.../MCPServersPage/components/MCPServerForm.tsx | 5 ++++-
3 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/site/src/pages/AISettingsPage/MCPServersPage/MCPServersPage.stories.tsx b/site/src/pages/AISettingsPage/MCPServersPage/MCPServersPage.stories.tsx
index 5a407a4f43e..57425489448 100644
--- a/site/src/pages/AISettingsPage/MCPServersPage/MCPServersPage.stories.tsx
+++ b/site/src/pages/AISettingsPage/MCPServersPage/MCPServersPage.stories.tsx
@@ -1286,6 +1286,11 @@ export const UpdateOnlyOrgAdminCanUpdateMCPServer: Story = {
await expect(await canvas.findByLabelText(/display name/i)).toHaveValue(
"Coder",
);
+ // Pristine edit forms keep the submit disabled until a change is made.
+ await expect(
+ canvas.getByRole("button", { name: "Update server" }),
+ ).toBeDisabled();
+ await userEvent.type(canvas.getByLabelText(/display name/i), " v2");
await expect(
canvas.getByRole("button", { name: "Update server" }),
).toBeEnabled();
diff --git a/site/src/pages/AISettingsPage/MCPServersPage/UpdateMCPServerPage/UpdateMCPServerPageView.stories.tsx b/site/src/pages/AISettingsPage/MCPServersPage/UpdateMCPServerPage/UpdateMCPServerPageView.stories.tsx
index f0f5cc61e96..d20ac50d309 100644
--- a/site/src/pages/AISettingsPage/MCPServersPage/UpdateMCPServerPage/UpdateMCPServerPageView.stories.tsx
+++ b/site/src/pages/AISettingsPage/MCPServersPage/UpdateMCPServerPage/UpdateMCPServerPageView.stories.tsx
@@ -58,6 +58,10 @@ export const Default: Story = {
);
const updateButton = canvas.getByRole("button", { name: "Update server" });
+ await expect(updateButton).toBeDisabled();
+ const displayName = canvas.getByLabelText(/display name/i);
+ await userEvent.clear(displayName);
+ await userEvent.type(displayName, "Coder v2");
await expect(updateButton).toBeEnabled();
await userEvent.click(updateButton);
@@ -65,7 +69,7 @@ export const Default: Story = {
expect(onUpdateServer).toHaveBeenCalledWith(
"mcp-coder",
expect.objectContaining({
- display_name: "Coder",
+ display_name: "Coder v2",
slug: "coder",
}),
);
diff --git a/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerForm.tsx b/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerForm.tsx
index 60b496db08c..783eda2d8ed 100644
--- a/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerForm.tsx
+++ b/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerForm.tsx
@@ -93,7 +93,10 @@ export const MCPServerForm: FC = ({
const isDisabled = isSaving || isDeleting;
const areFieldsDisabled =
isDisabled || (isEditing && onUpdateServer === undefined);
- const canSubmit = canSubmitMCPServerForm(form.values, areFieldsDisabled);
+ // Editing requires a change before submitting, matching the provider form.
+ const canSubmit =
+ canSubmitMCPServerForm(form.values, areFieldsDisabled) &&
+ (!isEditing || form.dirty);
const unsavedChanges = useUnsavedChangesPrompt(
form.dirty && !form.isSubmitting,
);
From edf22caea7d96e2e3f35f4db7baf931f3f868f51 Mon Sep 17 00:00:00 2001
From: Tracy Johnson
Date: Tue, 25 Aug 2026 16:42:55 +0000
Subject: [PATCH 08/19] feat(site/src/pages/AISettingsPage): use
content-secondary for disabled row names
---
.../AISettingsPage/MCPServersPage/components/MCPServerRow.tsx | 2 +-
.../src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerRow.tsx b/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerRow.tsx
index d73846c37d9..f79a4adc32c 100644
--- a/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerRow.tsx
+++ b/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerRow.tsx
@@ -31,7 +31,7 @@ export const MCPServerRow: FC = ({ server, onClick }) => {
{server.display_name}
diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx
index 75279869174..a6db8bbeee3 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx
@@ -75,7 +75,7 @@ export const ModelRow: FC = ({
"truncate text-sm font-medium leading-6",
model.enabled
? "text-content-primary"
- : "text-content-disabled",
+ : "text-content-secondary",
)}
title={displayName}
>
From eeee933cae05694f86f9bba916fcbea7411c4480 Mon Sep 17 00:00:00 2001
From: Tracy Johnson
Date: Tue, 25 Aug 2026 16:51:20 +0000
Subject: [PATCH 09/19] chore(site/src/pages/AISettingsPage): drop redundant
story and trim comments
---
.../MCPServersPage/MCPServersPage.stories.tsx | 10 ----
.../MCPServersPageView.stories.tsx | 4 --
.../ModelsPage/ModelsPageView.stories.tsx | 10 +---
.../UpdateModelPageView.stories.tsx | 5 +-
.../components/ModelForm.stories.tsx | 57 -------------------
.../components/ModelRow.stories.tsx | 22 +++----
6 files changed, 11 insertions(+), 97 deletions(-)
diff --git a/site/src/pages/AISettingsPage/MCPServersPage/MCPServersPage.stories.tsx b/site/src/pages/AISettingsPage/MCPServersPage/MCPServersPage.stories.tsx
index 57425489448..a33f3062a2e 100644
--- a/site/src/pages/AISettingsPage/MCPServersPage/MCPServersPage.stories.tsx
+++ b/site/src/pages/AISettingsPage/MCPServersPage/MCPServersPage.stories.tsx
@@ -569,12 +569,6 @@ export const ListSearchFiltersServers: Story = {
const canvas = within(canvasElement);
await expect(await canvas.findByText("Coder")).toBeVisible();
await expect(canvas.getByText("GitHub")).toBeVisible();
- await expect(
- canvas.getByRole("button", {
- name: `Organization ${MockDefaultOrganization.display_name}`,
- }),
- ).toBeVisible();
- expect(canvas.queryByText("Organization")).not.toBeInTheDocument();
const search = canvas.getByRole("searchbox", { name: "Search servers" });
await userEvent.type(search, "github");
@@ -1286,10 +1280,6 @@ export const UpdateOnlyOrgAdminCanUpdateMCPServer: Story = {
await expect(await canvas.findByLabelText(/display name/i)).toHaveValue(
"Coder",
);
- // Pristine edit forms keep the submit disabled until a change is made.
- await expect(
- canvas.getByRole("button", { name: "Update server" }),
- ).toBeDisabled();
await userEvent.type(canvas.getByLabelText(/display name/i), " v2");
await expect(
canvas.getByRole("button", { name: "Update server" }),
diff --git a/site/src/pages/AISettingsPage/MCPServersPage/MCPServersPageView.stories.tsx b/site/src/pages/AISettingsPage/MCPServersPage/MCPServersPageView.stories.tsx
index 256cf907824..e659a8864a3 100644
--- a/site/src/pages/AISettingsPage/MCPServersPage/MCPServersPageView.stories.tsx
+++ b/site/src/pages/AISettingsPage/MCPServersPage/MCPServersPageView.stories.tsx
@@ -59,12 +59,8 @@ export const Default: Story = {
await expect(canvas.getByText("Image")).toBeInTheDocument();
await expect(canvas.getByText("API key")).toBeInTheDocument();
expect(canvas.queryByText("Enabled")).not.toBeInTheDocument();
- // The disabled badge renders beside the server name, not in a status
- // column.
const disabledRow = canvas.getByRole("button", { name: /Image/i });
await expect(within(disabledRow).getByText("Disabled")).toBeInTheDocument();
- const enabledRow = canvas.getByRole("button", { name: /Memory/i });
- expect(within(enabledRow).queryByText("Disabled")).not.toBeInTheDocument();
},
};
diff --git a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx
index 06f3db7d7bc..6bbba1d6247 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/ModelsPageView.stories.tsx
@@ -96,12 +96,8 @@ export const Default: Story = {
// visible label asserted above rather than the image alt text.
expect(canvas.queryByText("Enabled")).not.toBeInTheDocument();
await expect(canvas.getByText("Default")).toBeInTheDocument();
- // The disabled badge renders beside the model name, not in a status
- // column.
const disabledRow = canvas.getByRole("button", { name: /GPT-4o mini/i });
await expect(within(disabledRow).getByText("Disabled")).toBeInTheDocument();
- const enabledRow = canvas.getByRole("button", { name: /^GPT-5/i });
- expect(within(enabledRow).queryByText("Disabled")).not.toBeInTheDocument();
// The Add model menu lists each provider by exact accessible name; a
// regressed icon would turn a name into "Anthropic Anthropic".
@@ -189,10 +185,8 @@ export const DisabledProviderModelsStillListed: Story = {
// so the row is queried by its clickable role.
const row = canvas.getByRole("button", { name: /GPT-4o Secondary/i });
await expect(within(row).getByText("OpenAI Secondary")).toBeInTheDocument();
- // A model under a disabled provider is not usable, so the status cell
- // must show the unavailable notice even though the stored enabled flag
- // is true. Scope to the target row so a fixture change cannot pass
- // this assertion via an unrelated cell.
+ // A model under a disabled provider is not usable regardless of its
+ // stored enabled flag; scope to the row so an unrelated cell cannot pass.
await expect(within(row).getByText("Unavailable")).toBeInTheDocument();
expect(within(row).queryByText("Disabled")).not.toBeInTheDocument();
},
diff --git a/site/src/pages/AISettingsPage/ModelsPage/UpdateModelPage/UpdateModelPageView.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/UpdateModelPage/UpdateModelPageView.stories.tsx
index 0e4dcfc288d..40cc0ca9f02 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/UpdateModelPage/UpdateModelPageView.stories.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/UpdateModelPage/UpdateModelPageView.stories.tsx
@@ -73,9 +73,8 @@ export const Default: Story = {
canvas.getByRole("button", { name: /^update model$/i }),
).toBeVisible();
await expect(canvas.getByLabelText(/model identifier/i)).toBeEnabled();
- // The organization is informational on the edit page: switching org
- // while editing would 404 the model, so it renders as a static value
- // rather than a picker.
+ // Switching org while editing would 404 the model, so the organization
+ // renders as a static value rather than a picker.
await expect(
canvas.getByLabelText(
`Organization ${MockDefaultOrganization.display_name}`,
diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelForm.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelForm.stories.tsx
index a6636bc9bb2..d6a41382d2a 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelForm.stories.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelForm.stories.tsx
@@ -1,5 +1,4 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
-import { useLocation } from "react-router";
import {
expect,
fn,
@@ -20,7 +19,6 @@ import {
} from "#/testHelpers/chatModels";
import {
MockDefaultOrganization,
- MockOrganization2,
MockOrganizationPermissions,
} from "#/testHelpers/entities";
import { withDashboardProvider, withToaster } from "#/testHelpers/storybook";
@@ -1062,58 +1060,3 @@ export const UseResponsesAPIHiddenForAzure: Story = {
await expect(canvas.getByLabelText(/temperature/i)).toBeInTheDocument();
},
};
-
-const LocationProbe = () => {
- const location = useLocation();
- return (
-
- {location.pathname}
- {location.search}
-
- );
-};
-
-export const SwitchOrganizationFromFormRow: Story = {
- decorators: [
- (Story) => (
-
-
-
- ),
- ],
- render: (args) => (
- <>
-
-
- >
- ),
- play: async ({ canvasElement }) => {
- const canvas = within(canvasElement);
- await expect(canvas.getByText("Organization")).toBeVisible();
- await userEvent.click(
- canvas.getByRole("button", {
- name: new RegExp(
- `Organization ${MockDefaultOrganization.display_name}`,
- "i",
- ),
- }),
- );
- await userEvent.click(
- await screen.findByRole("option", {
- name: new RegExp(MockOrganization2.display_name),
- }),
- );
- await waitFor(() => {
- expect(screen.getByTestId("location-probe")).toHaveTextContent(
- `/ai/settings/models/add?org=${MockOrganization2.name}`,
- );
- });
- },
-};
diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.stories.tsx
index bfe39091d69..2ad25c37f83 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.stories.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.stories.tsx
@@ -32,9 +32,7 @@ const meta: Meta = {
export default meta;
type Story = StoryObj;
-// Control case: with an enabled provider and an enabled model, the row
-// renders no status badge at all. Any regression that surfaces a badge for a
-// healthy model shows up here.
+// Control case: a healthy, enabled model renders no status badge at all.
export const WithProvider: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
@@ -46,11 +44,8 @@ export const WithProvider: Story = {
},
};
-// When the provider is missing (soft-deleted or otherwise unavailable) the
-// Provider column shows "Unset" and the status cell shows an "Unavailable"
-// notice 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 },
@@ -73,9 +68,8 @@ export const WithoutProviderForcesDisabled: Story = {
},
};
-// When the provider exists but is disabled, the label still renders (the
-// provider is set) but the status cell shows the "Unavailable" notice with a
-// tooltip 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 },
@@ -99,10 +93,8 @@ export const DisabledProviderForcesDisabled: Story = {
},
};
-// A disabled model with an enabled provider shows the "Disabled" badge next
-// to the name (like the "Default" badge) with no status-cell notice. 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 },
From 25434b188f42623ba13e0c93b6da29599b25f01f Mon Sep 17 00:00:00 2001
From: Tracy Johnson
Date: Tue, 25 Aug 2026 17:16:56 +0000
Subject: [PATCH 10/19] chore(site/src): tighten organization select doc
comments
---
.../OrganizationAutocomplete.tsx | 9 +++------
.../ModelsPage/components/ModelOrganizationSelect.tsx | 7 +++----
2 files changed, 6 insertions(+), 10 deletions(-)
diff --git a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx
index ce7b5a443d8..27103cbc6f7 100644
--- a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx
+++ b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx
@@ -157,17 +157,14 @@ export const OrganizationAutocomplete: FC = ({
type OrganizationValueProps = {
organization: Organization;
- // Collision set for disambiguation labels, matching the autocomplete's
- // labelOrganizations behavior. Defaults to just the organization.
labelOrganizations?: readonly Organization[];
id?: string;
className?: string;
};
-// Read-only counterpart to OrganizationAutocomplete for contexts where the
-// organization is informational and cannot be changed. Renders a static
-// value instead of a disabled control so the text keeps its normal color
-// and stays out of the tab order.
+// Read-only counterpart to OrganizationAutocomplete: a static value rather
+// than a disabled control, so it keeps normal text color and stays out of
+// the tab order.
export const OrganizationValue: FC = ({
organization,
labelOrganizations,
diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelOrganizationSelect.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelOrganizationSelect.tsx
index d92dd4b46ae..76a3cb93806 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/components/ModelOrganizationSelect.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/components/ModelOrganizationSelect.tsx
@@ -11,10 +11,9 @@ import {
useOrganizationModels,
} from "../organizationModels";
-// Switches the active organization by rewriting the org search param while
-// preserving the current path and auxiliary parameters. Hidden when only one
-// organization is accessible. With readOnly the organization renders as an
-// informational value instead, and is shown even for a single organization.
+// Switches the active organization by rewriting the org search param in
+// place. Hidden with a single accessible organization unless readOnly,
+// which renders a static value instead.
export const ModelOrganizationSelect: FC<{
label?: string;
readOnly?: boolean;
From 4c8e5fd1b85948fedc5a26daf50cdb5936800930 Mon Sep 17 00:00:00 2001
From: Tracy Johnson
Date: Tue, 25 Aug 2026 17:20:54 +0000
Subject: [PATCH 11/19] fix(site/src/components/OrganizationAutocomplete):
match select trigger chevron behavior
---
.../OrganizationAutocomplete/OrganizationAutocomplete.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx
index 27103cbc6f7..171686a6fcd 100644
--- a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx
+++ b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx
@@ -92,7 +92,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,
)}
>
@@ -112,7 +112,7 @@ export const OrganizationAutocomplete: FC = ({
Select an organization{required ? "…" : " (optional)"}
)}
-
+
Date: Tue, 25 Aug 2026 17:22:17 +0000
Subject: [PATCH 12/19] chore(site): tighten org picker comments
---
.../OrganizationAutocomplete/OrganizationAutocomplete.tsx | 4 +---
.../MCPServersPage/components/OrganizationPicker.tsx | 5 +----
.../ModelsPage/components/ModelOrganizationSelect.tsx | 4 +---
.../pages/AISettingsPage/ModelsPage/components/ModelRow.tsx | 4 +---
4 files changed, 4 insertions(+), 13 deletions(-)
diff --git a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx
index 171686a6fcd..0acc9af905e 100644
--- a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx
+++ b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx
@@ -162,9 +162,7 @@ type OrganizationValueProps = {
className?: string;
};
-// Read-only counterpart to OrganizationAutocomplete: a static value rather
-// than a disabled control, so it keeps normal text color and stays out of
-// the tab order.
+// Static read-only counterpart to OrganizationAutocomplete.
export const OrganizationValue: FC = ({
organization,
labelOrganizations,
diff --git a/site/src/pages/AISettingsPage/MCPServersPage/components/OrganizationPicker.tsx b/site/src/pages/AISettingsPage/MCPServersPage/components/OrganizationPicker.tsx
index 33edf1a074c..25e45e5998d 100644
--- a/site/src/pages/AISettingsPage/MCPServersPage/components/OrganizationPicker.tsx
+++ b/site/src/pages/AISettingsPage/MCPServersPage/components/OrganizationPicker.tsx
@@ -48,13 +48,10 @@ export const OrganizationPicker: FC = ({
organization,
labelOrganizations,
);
- // A picker without a change handler or alternatives is informational, so
- // render a static value instead of a disabled control with muted text.
+ // A picker without a change handler or alternatives is informational.
const isReadOnly = !onChange || hasSingleSelectedOrganization;
return (
- // The label and gap mirror the form Field primitive so the picker
- // lines up with sibling fields when rendered inside a form grid.
{showLabel && (
diff --git a/site/src/pages/AISettingsPage/ModelsPage/AddModelPage/AddModelPageView.tsx b/site/src/pages/AISettingsPage/ModelsPage/AddModelPage/AddModelPageView.tsx
index 91175b789d6..07a0ed28c90 100644
--- a/site/src/pages/AISettingsPage/ModelsPage/AddModelPage/AddModelPageView.tsx
+++ b/site/src/pages/AISettingsPage/ModelsPage/AddModelPage/AddModelPageView.tsx
@@ -42,6 +42,7 @@ const AddModelPageView: FC = ({
+
);
}
diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.stories.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.stories.tsx
index 2ad25c37f83..967f323b920 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";
@@ -52,19 +52,23 @@ export const WithoutProviderForcesDisabled: Story = {
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("Unavailable")).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.getByText("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();
},
};
diff --git a/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx b/site/src/pages/AISettingsPage/ModelsPage/components/ModelRow.tsx
index dd6e2a7ca5d..47932413253 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";
@@ -126,22 +126,11 @@ export const ModelRow: FC = ({
{providerLabel}
) : (
-
-
- Unset
-
-
-
-
-
-
- The provider connected to this model has been deleted.
-
-
-
+ // The Unavailable badge beside the name explains the missing
+ // provider, so "Unset" needs no tooltip of its own.
+
+ Unset
+
)}
From f337a74372c94ce33ab6be5bb88b94fc4471ada7 Mon Sep 17 00:00:00 2001
From: Tracy Johnson
Date: Wed, 26 Aug 2026 14:25:15 +0000
Subject: [PATCH 14/19] fix(site/src/pages/AISettingsPage): address review
comments
---
.../OrganizationAutocomplete.tsx | 2 +-
.../MCPServersPage/MCPServersPage.stories.tsx | 10 ++++-
.../MCPServersPage/MCPServersPageView.tsx | 24 +++++-----
.../UpdateMCPServerPageView.stories.tsx | 7 ++-
.../components/MCPServerFormHeader.tsx | 43 +++++++++++++-----
.../AddModelPage/AddModelPageView.stories.tsx | 45 ++++++++++++++++++-
.../ModelsPage/components/ModelRow.tsx | 3 --
7 files changed, 103 insertions(+), 31 deletions(-)
diff --git a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx
index 0acc9af905e..5de3501901f 100644
--- a/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx
+++ b/site/src/components/OrganizationAutocomplete/OrganizationAutocomplete.tsx
@@ -112,7 +112,7 @@ export const OrganizationAutocomplete: FC = ({
Select an organization{required ? "…" : " (optional)"}
)}
-
+ = ({
}) => {
const navigate = useNavigate();
const [searchQuery, setSearchQuery] = useState("");
- const filteredServers = useMemo(() => {
- const normalizedQuery = searchQuery.trim().toLowerCase();
- if (normalizedQuery.length === 0) {
- return servers;
- }
- return servers.filter((server) =>
- [server.display_name, server.slug, server.url]
- .join(" ")
- .toLowerCase()
- .includes(normalizedQuery),
- );
- }, [servers, searchQuery]);
+ const normalizedQuery = searchQuery.trim().toLowerCase();
+ const filteredServers =
+ normalizedQuery.length === 0
+ ? servers
+ : servers.filter((server) =>
+ [server.display_name, server.slug, server.url]
+ .join(" ")
+ .toLowerCase()
+ .includes(normalizedQuery),
+ );
// Disambiguate against every organization sharing the page context:
// other creation targets and the currently selected organization.
const addButtonLabel =
diff --git a/site/src/pages/AISettingsPage/MCPServersPage/UpdateMCPServerPage/UpdateMCPServerPageView.stories.tsx b/site/src/pages/AISettingsPage/MCPServersPage/UpdateMCPServerPage/UpdateMCPServerPageView.stories.tsx
index cd79ef962fa..7fb53b57bbd 100644
--- a/site/src/pages/AISettingsPage/MCPServersPage/UpdateMCPServerPage/UpdateMCPServerPageView.stories.tsx
+++ b/site/src/pages/AISettingsPage/MCPServersPage/UpdateMCPServerPage/UpdateMCPServerPageView.stories.tsx
@@ -108,6 +108,7 @@ export const DeleteOnly: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
+ const body = within(canvasElement.ownerDocument.body);
await expect(canvas.getByLabelText(/display name/i)).toBeDisabled();
await expect(canvas.getByLabelText(/^slug/i)).toBeDisabled();
await expect(canvas.getByLabelText(/server url/i)).toBeDisabled();
@@ -117,10 +118,14 @@ export const DeleteOnly: Story = {
const enabledSwitch = canvas.getByRole("switch", {
name: "Server enabled",
});
- await expect(enabledSwitch).toBeDisabled();
+ await expect(enabledSwitch).toHaveAttribute("aria-disabled", "true");
await expect(enabledSwitch).toHaveAccessibleDescription(
"You do not have permission to update this server.",
);
+ enabledSwitch.focus();
+ await expect(await body.findByRole("tooltip")).toHaveTextContent(
+ "You do not have permission to update this server.",
+ );
await expect(canvas.getByRole("button", { name: "Delete" })).toBeEnabled();
},
};
diff --git a/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerFormHeader.tsx b/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerFormHeader.tsx
index 579b99602cb..94ef6b98805 100644
--- a/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerFormHeader.tsx
+++ b/site/src/pages/AISettingsPage/MCPServersPage/components/MCPServerFormHeader.tsx
@@ -6,6 +6,11 @@ import { Badge } from "#/components/Badge/Badge";
import { Button } from "#/components/Button/Button";
import { SettingsHeaderTitle } from "#/components/SettingsHeader/SettingsHeader";
import { Switch } from "#/components/Switch/Switch";
+import {
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
+} from "#/components/Tooltip/Tooltip";
import { cn } from "#/utils/cn";
import { MCPServerIcon } from "./MCPServerIcon";
@@ -42,6 +47,7 @@ export const MCPServerFormHeader: FC = ({
onToggleEnabled,
}) => {
const disabledReasonId = useId();
+ const lacksUpdatePermission = isEditing && server && !onToggleEnabled;
return (
<>
@@ -81,16 +87,33 @@ export const MCPServerFormHeader: FC = ({
Disabled servers are hidden from agents.