Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
a16b6c4
feat(site): reposition org pickers in AI settings models and MCP pages
tracyjohnsonux Aug 25, 2026
98cfedd
feat(site): replace list status columns with inline disabled badges
tracyjohnsonux Aug 25, 2026
76e16ae
feat(site): make the organization informational on the model edit page
tracyjohnsonux Aug 25, 2026
9f2282b
feat(site/src/pages/AISettingsPage/MCPServersPage): match provider en…
tracyjohnsonux Aug 25, 2026
ca93e43
feat(site): dim disabled model and MCP server rows like providers
tracyjohnsonux Aug 25, 2026
2267037
feat(site): move the model unavailable notice into the name cell
tracyjohnsonux Aug 25, 2026
dc507b2
feat(site/src/pages/AISettingsPage/MCPServersPage): disable update su…
tracyjohnsonux Aug 25, 2026
edf22ca
feat(site/src/pages/AISettingsPage): use content-secondary for disabl…
tracyjohnsonux Aug 25, 2026
eeee933
chore(site/src/pages/AISettingsPage): drop redundant story and trim c…
tracyjohnsonux Aug 25, 2026
25434b1
chore(site/src): tighten organization select doc comments
tracyjohnsonux Aug 25, 2026
4c8e5fd
fix(site/src/components/OrganizationAutocomplete): match select trigg…
tracyjohnsonux Aug 25, 2026
5061263
chore(site): tighten org picker comments
tracyjohnsonux Aug 25, 2026
c011f2d
fix(site/src/pages/AISettingsPage): address review findings on search…
tracyjohnsonux Aug 25, 2026
eb2789d
Merge branch 'main' into feat/models-org-picker-placement
tracyjohnsonux Aug 25, 2026
3474df1
Merge branch 'main' into feat/models-org-picker-placement
tracyjohnsonux Aug 25, 2026
94cc3a2
Merge branch 'main' into feat/models-org-picker-placement
tracyjohnsonux Aug 25, 2026
f337a74
fix(site/src/pages/AISettingsPage): address review comments
tracyjohnsonux Aug 26, 2026
3103e9f
fix(site/src/pages/AISettingsPage): fix model badge a11y and org filt…
ibetitsmike Aug 26, 2026
50c7d4f
docs(docs/ai-coder/agents): describe unavailable model badges
ibetitsmike Aug 26, 2026
51c7e4d
fix(site/src/pages/AISettingsPage): address Mike review feedback
tracyjohnsonux Aug 26, 2026
7a0f444
refactor(site/src/pages/AISettingsPage): inline model organization se…
tracyjohnsonux Aug 26, 2026
6b730c4
Merge branch 'main' into feat/models-org-picker-placement
tracyjohnsonux Aug 26, 2026
bf56091
refactor(site/src/components): share organization field presentation
tracyjohnsonux Aug 26, 2026
5ddfd31
Merge branch 'main' into feat/models-org-picker-placement
tracyjohnsonux Aug 26, 2026
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
10 changes: 5 additions & 5 deletions docs/ai-coder/agents/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -210,11 +210,11 @@
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
Expand Down Expand Up @@ -373,7 +373,7 @@
enabled, developers can supply personal API keys for any enabled AI provider
from the Agents settings page.

### Managing personal API keys

Check warning on line 376 in docs/ai-coder/agents/models.md

View workflow job for this annotation

GitHub Actions / lint-docs

Coder.GerundHeading

Heading starts with an -ing word ('Managing'); prefer the imperative ('Install') or the noun ('Installation'). See capitalization-and-punctuation.md#no-gerund-leading-headings.

1. Navigate to the **Agents** page in the Coder dashboard.
1. Open **Settings** and select the **API Keys** tab.
Expand All @@ -390,14 +390,14 @@
used for deployment-managed provider secrets. The dashboard never displays a
saved key, only whether one is set.

### Removing a personal key

Check warning on line 393 in docs/ai-coder/agents/models.md

View workflow job for this annotation

GitHub Actions / lint-docs

Coder.GerundHeading

Heading starts with an -ing word ('Removing'); prefer the imperative ('Install') or the noun ('Installation'). See capitalization-and-punctuation.md#no-gerund-leading-headings.

Click **Remove** on the provider card in the API Keys settings tab. Subsequent
requests use deployment-managed credentials when they are configured for that
provider. If no deployment-managed credential is available, add a new personal
key before you use models from that provider.

## Using an LLM proxy

Check warning on line 400 in docs/ai-coder/agents/models.md

View workflow job for this annotation

GitHub Actions / lint-docs

Coder.GerundHeading

Heading starts with an -ing word ('Using'); prefer the imperative ('Install') or the noun ('Installation'). See capitalization-and-punctuation.md#no-gerund-leading-headings.

Organizations that route LLM traffic through a centralized proxy, such as
LiteLLM or an internal gateway, can point a provider's **Endpoint** or **Base
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
CommandItem,
CommandList,
} from "#/components/Command/Command";
import { Label } from "#/components/Label/Label";
import {
Popover,
PopoverContent,
Expand Down Expand Up @@ -92,7 +93,7 @@ export const OrganizationAutocomplete: FC<OrganizationAutocompleteProps> = ({
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,
)}
>
Expand Down Expand Up @@ -154,3 +155,129 @@ export const OrganizationAutocomplete: FC<OrganizationAutocompleteProps> = ({
</Popover>
);
};

type OrganizationValueProps = {
organization: Organization;
labelOrganizations?: readonly Organization[];
id?: string;
className?: string;
};

const OrganizationValue: FC<OrganizationValueProps> = ({
organization,
labelOrganizations,
id,
className,
}) => {
const label = getOrganizationLabel(
organization,
labelOrganizations ?? [organization],
);
return (
<div
id={id}
role="group"
aria-label={`Organization ${label}`}
className={cn(
"flex h-10 items-center gap-2 rounded-md border border-solid border-border px-3 py-2 text-sm text-content-primary",
className,
)}
>
<Avatar
size="sm"
src={organization.icon}
fallback={organization.display_name}
/>
<span className="truncate">{label}</span>
</div>
);
};

type OrganizationFieldProps = {
id: string;
organization: Organization;
organizations: readonly Organization[];
labelOrganizations?: readonly Organization[];
onChange?: (organization: Organization) => void;
className?: string;
disabled?: boolean;
label?: string;
showLabel?: boolean;
showSingleOrganization?: boolean;
readOnly?: boolean;
triggerClassName?: string;
optionsTabbable?: boolean;
required?: boolean;
};

export const OrganizationField: FC<OrganizationFieldProps> = ({
id,
organization,
organizations,
labelOrganizations,
onChange,
className,
disabled,
label = "Organization",
showLabel = true,
showSingleOrganization = false,
readOnly = false,
triggerClassName,
optionsTabbable,
required = true,
}) => {
const hasSingleSelectedOrganization =
organizations.length <= 1 &&
organizations.some((option) => option.id === organization.id);
if (hasSingleSelectedOrganization && !showSingleOrganization && !readOnly) {
return null;
}

const resolvedLabelOrganizations =
labelOrganizations ??
(organizations.some((option) => option.id === organization.id)
? organizations
: [...organizations, organization]);
const organizationLabel = getOrganizationLabel(
organization,
resolvedLabelOrganizations,
);
const isReadOnly = readOnly || !onChange || hasSingleSelectedOrganization;

return (
<div className={cn("flex w-72 flex-col gap-1.5", className)}>
{showLabel && (
<Label
htmlFor={id}
className="flex items-center gap-1 leading-6 text-content-primary"
>
{label}
</Label>
)}
{isReadOnly ? (
<OrganizationValue
id={id}
organization={organization}
labelOrganizations={resolvedLabelOrganizations}
/>
) : (
<OrganizationAutocomplete
id={id}
ariaLabel={`${label} ${organizationLabel}`}
value={organization}
onChange={(org) => {
if (org) {
onChange?.(org);
}
}}
options={organizations}
labelOrganizations={resolvedLabelOrganizations}
required={required}
disabled={disabled}
triggerClassName={triggerClassName}
optionsTabbable={optionsTabbable}
/>
)}
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ export const Default: Story = {
const addButton = canvas.getByRole("button", { name: "Add server" });

await expect(
canvas.getByRole("button", {
name: `Organization ${MockDefaultOrganization.display_name}`,
}),
canvas.getByLabelText(
`Organization ${MockDefaultOrganization.display_name}`,
),
).toBeVisible();
await expect(addButton).toBeDisabled();
await userEvent.type(canvas.getByLabelText(/display name/i), "GitHub");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,32 +34,46 @@ const AddMCPServerPageView: FC<AddMCPServerPageViewProps> = ({
return (
<>
<title>{pageTitle("Add server", "AI Settings")}</title>
<OrganizationPicker
id="mcp-add-organization"
className="mb-6"
organizations={organizations}
organization={organization}
onChange={onSelectOrganization}
disabled={isSaving}
showSingleOrganization
/>
{canCreate ? (
<MCPServerForm
listPath={
canViewServerList ? mcpServersPath(organization) : undefined
}
isSaving={isSaving}
canSelectUserOIDC={canSelectUserOIDC}
organizationPicker={
<OrganizationPicker
id="mcp-add-organization"
className="w-full"
organizations={organizations}
organization={organization}
onChange={onSelectOrganization}
disabled={isSaving}
showSingleOrganization
/>
}
onCreateServer={onCreateServer}
onCancel={canViewServerList ? onCancel : undefined}
/>
) : (
<Alert severity="error" prominent>
<AlertTitle>You cannot add servers to this organization</AlertTitle>
<AlertDescription>
Choose an organization where you have permission to add MCP servers.
</AlertDescription>
</Alert>
<>
<OrganizationPicker
id="mcp-add-organization"
className="mb-6"
organizations={organizations}
organization={organization}
onChange={onSelectOrganization}
disabled={isSaving}
showSingleOrganization
/>
<Alert severity="error" prominent>
<AlertTitle>You cannot add servers to this organization</AlertTitle>
<AlertDescription>
Choose an organization where you have permission to add MCP
servers.
</AlertDescription>
</Alert>
</>
)}
</>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
import AddMCPServerPage from "./AddMCPServerPage/AddMCPServerPage";
import MCPServersPage from "./MCPServersPage";
import { orgSearchParam } from "./organizationParam";
import { MockCoderMCPServer } from "./testFixtures";
import { MockCoderMCPServer, MockGitHubMCPServer } from "./testFixtures";
import UpdateMCPServerPage from "./UpdateMCPServerPage/UpdateMCPServerPage";

const MockOrganization2MCPServer: TypesGen.MCPServerConfig = {
Expand Down Expand Up @@ -580,15 +580,55 @@ export const AddDeepLinkShowsSingleCreatableOrganization: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const organization = await canvas.findByRole("button", {
name: `Organization ${MockOrganization2.display_name}`,
});
const organization = await canvas.findByLabelText(
`Organization ${MockOrganization2.display_name}`,
);
await expect(organization).toBeVisible();
await expect(organization).toBeDisabled();
expect(
canvas.queryByRole("button", {
name: `Organization ${MockOrganization2.display_name}`,
}),
).not.toBeInTheDocument();
await expect(canvas.getByLabelText(/display name/i)).toBeVisible();
},
};

export const ListSearchFiltersServers: Story = {
parameters: {
organizations: [MockDefaultOrganization, MockOrganization2],
reactRouter: reactRouterParameters({
location: { path: "/ai/settings/mcp-servers" },
routing: { path: "/ai/settings/mcp-servers" },
}),
},
beforeEach: () => {
spyOn(API.experimental, "getMCPServerConfigs").mockResolvedValue([
MockCoderMCPServer,
MockGitHubMCPServer,
]);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
await expect(await canvas.findByText("Coder")).toBeVisible();
await expect(canvas.getByText("GitHub")).toBeVisible();

const search = canvas.getByRole("searchbox", { name: "Search servers" });
await userEvent.type(search, "github");
await expect(canvas.getByText("GitHub")).toBeVisible();
expect(canvas.queryByText("Coder")).not.toBeInTheDocument();

await userEvent.clear(search);
await userEvent.type(search, "no-such-server");
await expect(
canvas.getByText("No servers match your search"),
).toBeVisible();

await userEvent.clear(search);
await expect(canvas.getByText("Coder")).toBeVisible();
await expect(canvas.getByText("GitHub")).toBeVisible();
},
};

export const ListSwitchesOrganization: Story = {
parameters: {
organizations: [MockDefaultOrganization, MockOrganization2],
Expand Down Expand Up @@ -1282,6 +1322,7 @@ export const UpdateOnlyOrgAdminCanUpdateMCPServer: Story = {
await expect(await canvas.findByLabelText(/display name/i)).toHaveValue(
"Coder",
);
await userEvent.type(canvas.getByLabelText(/display name/i), " v2");
await expect(
canvas.getByRole("button", { name: "Update server" }),
).toBeEnabled();
Expand All @@ -1301,7 +1342,7 @@ export const UpdateOnlyOrgAdminCanUpdateMCPServer: Story = {
body.queryByRole("option", { name: "User OIDC identity" }),
).not.toBeInTheDocument();
expect(
canvas.queryByRole("button", { name: "Server actions" }),
canvas.queryByRole("button", { name: "Delete" }),
).not.toBeInTheDocument();
expect(
canvas.queryByRole("button", { name: /delete server/i }),
Expand Down Expand Up @@ -1375,7 +1416,6 @@ export const UserOIDCOrgAdminCannotUpdate: Story = {
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const body = within(canvasElement.ownerDocument.body);
await expect(await canvas.findByLabelText(/display name/i)).toHaveValue(
"Coder",
);
Expand All @@ -1399,12 +1439,7 @@ export const UserOIDCOrgAdminCannotUpdate: Story = {
await expect(
canvas.getByLabelText(/authentication method/i),
).toHaveTextContent("User OIDC identity");
await userEvent.click(
canvas.getByRole("button", { name: "Server actions" }),
);
await expect(
await body.findByRole("menuitem", { name: "Remove" }),
).toBeEnabled();
await expect(canvas.getByRole("button", { name: "Delete" })).toBeEnabled();
},
};

Expand Down Expand Up @@ -1469,12 +1504,7 @@ export const DeleteOnlyOrgAdminCanDeleteWithoutUpdating: Story = {
).toBeDisabled();
await expect(canvas.getByLabelText(/tool allow list/i)).toBeDisabled();
await expect(canvas.getByLabelText(/tool deny list/i)).toBeDisabled();
await userEvent.click(
canvas.getByRole("button", { name: "Server actions" }),
);
await userEvent.click(
await body.findByRole("menuitem", { name: "Remove" }),
);
await userEvent.click(canvas.getByRole("button", { name: "Delete" }));
await userEvent.click(
await body.findByRole("button", { name: "Delete MCP server" }),
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ const MCPServersPage: FC = () => {
)}
{organization && (
<MCPServersPageView
// Reset view-local state (search) when the organization changes.
key={organization.id}
isLoading={serversQuery.isLoading}
error={serversQuery.error}
servers={servers}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ export const Default: Story = {
await expect(canvas.getByText("GitHub")).toBeInTheDocument();
await expect(canvas.getByText("Image")).toBeInTheDocument();
await expect(canvas.getByText("API key")).toBeInTheDocument();
await expect(canvas.getAllByText("Enabled").length).toBeGreaterThan(0);
await expect(canvas.getByText("Disabled")).toBeInTheDocument();
expect(canvas.queryByText("Enabled")).not.toBeInTheDocument();
const disabledRow = canvas.getByRole("button", { name: /Image/i });
await expect(within(disabledRow).getByText("Disabled")).toBeInTheDocument();
},
};

Expand Down
Loading
Loading