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

Skip to content
Closed
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
10 changes: 5 additions & 5 deletions docs/ai-coder/agents/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,11 +206,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 @@ -369,7 +369,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 372 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 @@ -386,14 +386,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 389 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 396 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 @@ -538,15 +538,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 @@ -1240,6 +1280,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 @@ -1259,7 +1300,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 @@ -1333,7 +1374,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 @@ -1357,12 +1397,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 @@ -1427,12 +1462,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 @@ -99,6 +99,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