fix(site/src/pages/AgentsPage): use sentence case for UI labels (#25941)

Converts all title-case UI labels in the Coder Agents area to sentence
case for consistency. Also renames the "New Agent" sidebar button to
"New chat".

## Changes

### Settings headings
| Before | After |
|---|---|
| Personal Instructions | Personal instructions |
| Chat Layout | Chat layout |
| Keyboard Shortcuts | Keyboard shortcuts |
| Thinking Display | Thinking display |
| Shell Output Display | Shell output display |
| Code Diff Display | Code diff display |
| Autostop Fallback | Autostop fallback |
| Workspace Autostop Fallback | Workspace autostop fallback |
| Auto-Archive Inactive Conversations | Auto-archive inactive
conversations |
| Conversation Retention Period | Conversation retention period |
| Chat Debug Data Retention | Chat debug data retention |
| System Instructions | System instructions |
| Context Compaction | Context compaction |
| Cost Tracking | Cost tracking |
| Provider Configuration | Provider configuration |
| Virtual Desktop | Virtual desktop |

### Select/option labels
| Before | After |
|---|---|
| Always Expanded | Always expanded |
| Always Collapsed | Always collapsed |

### Sidebar and nav labels
| Before | After |
|---|---|
| New Agent | New chat |
| Personal Skills | Personal skills |
| Manage Agents | Manage agents |
| MCP Servers | MCP servers |
| Back to Settings | Back to settings |
| Back to Agents | Back to agents |

### Form field labels
| Before | After |
|---|---|
| Display Name | Display name |
| Client Secret | Client secret |
| Header Name | Header name |
| Tool Allow List | Tool allow list |
| Tool Deny List | Tool deny list |
| Spend Limit | Spend limit |
| Cache Read | Cache read |
| Cache Write | Cache write |
| Model Identifier | Model identifier |
| Context Limit | Context limit |
| Compression Threshold | Compression threshold |

### Model form titles
| Before | After |
|---|---|
| Add Model | Add model |
| Edit Model | Edit model |
| Duplicate Model | Duplicate model |

### Admin/limits labels
| Before | After |
|---|---|
| Group Limits | Group limits |
| Per-User Overrides | Per-user overrides |
| Default Spend Limit | Default spend limit |

### Other
| Before | After |
|---|---|
| Weekly/Workspace Usage | Weekly/Workspace usage |
| View Usage | View usage |
| attached image / attached file | Attached image / Attached file |

### Not changed (server-provided labels)

Model config field labels like "Reasoning Effort", "Max Completion
Tokens", "Send Reasoning", etc. are provided by the server via
`field.label` and rendered as-is by `snakeToPrettyLabel`. These require
a server-side change to use sentence case.

All corresponding story and test assertions updated to match.

> 🤖 Generated by Coder Agents on behalf of @tracyjohnsonux
This commit is contained in:
TJ
2026-06-02 07:17:06 -07:00
committed by GitHub
parent 9fe75587ae
commit b411f09383
37 changed files with 132 additions and 134 deletions
@@ -153,7 +153,7 @@ export const ForcedByDeployment: Story = {
export const DesktopSetting: Story = { export const DesktopSetting: Story = {
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
await canvas.findByText("Virtual Desktop"); await canvas.findByText("Virtual desktop");
await canvas.findByText( await canvas.findByText(
/Allow agents to use a virtual, graphical desktop within workspaces./i, /Allow agents to use a virtual, graphical desktop within workspaces./i,
); );
@@ -55,7 +55,7 @@ export const InvisibleUnicodeWarningUserPrompt: Story = {
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
await canvas.findByText("Personal Instructions"); await canvas.findByText("Personal instructions");
const alert = await canvas.findByText(/invisible Unicode/); const alert = await canvas.findByText(/invisible Unicode/);
expect(alert).toBeInTheDocument(); expect(alert).toBeInTheDocument();
expect(alert.textContent).toContain("2"); expect(alert.textContent).toContain("2");
@@ -128,7 +128,7 @@ export const RendersChatLayoutSection: Story = {
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(await canvas.findByText("Chat Layout")).toBeInTheDocument(); expect(await canvas.findByText("Chat layout")).toBeInTheDocument();
expect( expect(
await canvas.findByRole("switch", { name: "Full-width chat" }), await canvas.findByRole("switch", { name: "Full-width chat" }),
).toBeInTheDocument(); ).toBeInTheDocument();
@@ -160,7 +160,7 @@ export const TogglesSendShortcut: Story = {
name: "Require Cmd/Ctrl+Enter to send messages", name: "Require Cmd/Ctrl+Enter to send messages",
}); });
expect(await canvas.findByText("Keyboard Shortcuts")).toBeInTheDocument(); expect(await canvas.findByText("Keyboard shortcuts")).toBeInTheDocument();
expect(toggle).not.toBeChecked(); expect(toggle).not.toBeChecked();
await userEvent.click(toggle); await userEvent.click(toggle);
@@ -177,9 +177,9 @@ export const RendersAgentDisplayModeSettings: Story = {
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
expect(await canvas.findByText("Thinking Display")).toBeVisible(); expect(await canvas.findByText("Thinking display")).toBeVisible();
expect(await canvas.findByText("Shell Output Display")).toBeVisible(); expect(await canvas.findByText("Shell output display")).toBeVisible();
expect(await canvas.findByText("Code Diff Display")).toBeVisible(); expect(await canvas.findByText("Code diff display")).toBeVisible();
}, },
}; };
@@ -117,7 +117,7 @@ export const InvisibleUnicodeWarningSystemPrompt: Story = {
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
await canvas.findByText("System Instructions"); await canvas.findByText("System instructions");
const alert = await canvas.findByText(/invisible Unicode/); const alert = await canvas.findByText(/invisible Unicode/);
expect(alert).toBeInTheDocument(); expect(alert).toBeInTheDocument();
expect(alert.textContent).toContain("4"); expect(alert.textContent).toContain("4");
@@ -138,7 +138,7 @@ export const NoWarningForCleanPrompt: Story = {
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
await canvas.findByText("System Instructions"); await canvas.findByText("System instructions");
await canvas.findByDisplayValue("You are a helpful coding assistant."); await canvas.findByDisplayValue("You are a helpful coding assistant.");
expect(canvas.queryByText(/invisible Unicode/)).toBeNull(); expect(canvas.queryByText(/invisible Unicode/)).toBeNull();
}, },
@@ -46,7 +46,7 @@ export const Default: Story = {};
export const DefaultAutostopDefault: Story = { export const DefaultAutostopDefault: Story = {
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
await canvas.findByText("Workspace Autostop Fallback"); await canvas.findByText("Workspace autostop fallback");
await canvas.findByText( await canvas.findByText(
/Set a default autostop for agent-created workspaces/i, /Set a default autostop for agent-created workspaces/i,
); );
@@ -55,7 +55,7 @@ export const DefaultAutostopDefault: Story = {
name: "Enable default autostop", name: "Enable default autostop",
}); });
expect(toggle).not.toBeChecked(); expect(toggle).not.toBeChecked();
expect(canvas.queryByLabelText("Autostop Fallback")).toBeNull(); expect(canvas.queryByLabelText("Autostop fallback")).toBeNull();
}, },
}; };
@@ -70,7 +70,7 @@ export const DefaultAutostopCustomValue: Story = {
}); });
expect(toggle).toBeChecked(); expect(toggle).toBeChecked();
const durationInput = await canvas.findByLabelText("Autostop Fallback"); const durationInput = await canvas.findByLabelText("Autostop fallback");
expect(durationInput).toHaveValue("2"); expect(durationInput).toHaveValue("2");
}, },
}; };
@@ -90,7 +90,7 @@ export const DefaultAutostopSave: Story = {
); );
}); });
const durationInput = await canvas.findByLabelText("Autostop Fallback"); const durationInput = await canvas.findByLabelText("Autostop fallback");
expect(durationInput).toHaveValue("1"); expect(durationInput).toHaveValue("1");
await userEvent.clear(durationInput); await userEvent.clear(durationInput);
@@ -126,7 +126,7 @@ export const DefaultAutostopExceedsMax: Story = {
}); });
await userEvent.click(toggle); await userEvent.click(toggle);
const durationInput = await canvas.findByLabelText("Autostop Fallback"); const durationInput = await canvas.findByLabelText("Autostop fallback");
const ttlForm = durationInput.closest("form"); const ttlForm = durationInput.closest("form");
if (!(ttlForm instanceof HTMLFormElement)) { if (!(ttlForm instanceof HTMLFormElement)) {
throw new Error( throw new Error(
@@ -180,7 +180,7 @@ export const DefaultAutostopSaveDisabled: Story = {
}); });
expect(toggle).toBeChecked(); expect(toggle).toBeChecked();
const durationInput = await canvas.findByLabelText("Autostop Fallback"); const durationInput = await canvas.findByLabelText("Autostop fallback");
expect(durationInput).toHaveValue("2"); expect(durationInput).toHaveValue("2");
const ttlForm = durationInput.closest("form"); const ttlForm = durationInput.closest("form");
@@ -229,7 +229,7 @@ export const DefaultAutostopToggleOffFailure: Story = {
}); });
expect(toggle).toBeChecked(); expect(toggle).toBeChecked();
const durationInput = await canvas.findByLabelText("Autostop Fallback"); const durationInput = await canvas.findByLabelText("Autostop fallback");
expect(durationInput).toHaveValue("2"); expect(durationInput).toHaveValue("2");
await userEvent.click(toggle); await userEvent.click(toggle);
@@ -634,7 +634,7 @@ export const RetentionBelowMin: Story = {
export const DebugRetentionLoadedDefault: Story = { export const DebugRetentionLoadedDefault: Story = {
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);
await canvas.findByText("Chat Debug Data Retention"); await canvas.findByText("Chat debug data retention");
await canvas.findByText(/debug runs and debug steps/i); await canvas.findByText(/debug runs and debug steps/i);
await canvas.findByText(/does not control chat message retention/i); await canvas.findByText(/does not control chat message retention/i);
@@ -23,7 +23,7 @@ const AgentSettingsMCPServersPage: FC = () => {
return ( return (
<RequirePermission isFeatureVisible={permissions.editDeploymentConfig}> <RequirePermission isFeatureVisible={permissions.editDeploymentConfig}>
<MCPServerAdminPanel <MCPServerAdminPanel
sectionLabel="MCP Servers" sectionLabel="MCP servers"
sectionDescription="Configure external MCP servers that provide additional tools for Coder Agents." sectionDescription="Configure external MCP servers that provide additional tools for Coder Agents."
serversData={serversQuery.data} serversData={serversQuery.data}
isLoadingServers={serversQuery.isLoading} isLoadingServers={serversQuery.isLoading}
@@ -11,7 +11,7 @@ const AgentSettingsPage: FC = () => {
const sidebarView = sidebarViewFromPath(location.pathname); const sidebarView = sidebarViewFromPath(location.pathname);
const mobileBack = section const mobileBack = section
? sidebarView.panel === "settings-admin" ? sidebarView.panel === "settings-admin"
? { to: "/agents/settings/admin", label: "Manage Agents" } ? { to: "/agents/settings/admin", label: "Manage agents" }
: { to: "/agents/settings", label: "Settings" } : { to: "/agents/settings", label: "Settings" }
: undefined; : undefined;
@@ -224,7 +224,7 @@ export const AgentSettingsPersonalSkillsPageView: FC<
return ( return (
<div className="flex flex-col gap-8"> <div className="flex flex-col gap-8">
<SectionHeader <SectionHeader
label="Personal Skills" label="Personal skills"
description="Reusable instructions your agents can pick when they need specialized guidance. Personal skills hold a single SKILL.md file. For richer skills with supporting files, add them to your repo under `.agents/skills/` or load them from a workspace." description="Reusable instructions your agents can pick when they need specialized guidance. Personal skills hold a single SKILL.md file. For richer skills with supporting files, add them to your repo under `.agents/skills/` or load them from a workspace."
action={addSkillAction} action={addSkillAction}
/> />
@@ -726,7 +726,7 @@ export const EmptyStateZoom200Desktop: Story = {
}); });
await expect(canvas.getByRole("link", { name: "Settings" })).toBeVisible(); await expect(canvas.getByRole("link", { name: "Settings" })).toBeVisible();
await expect(canvas.getByRole("link", { name: "New Agent" })).toBeVisible(); await expect(canvas.getByRole("link", { name: "New chat" })).toBeVisible();
await expect( await expect(
canvas.getByRole("button", { name: "Collapse sidebar" }), canvas.getByRole("button", { name: "Collapse sidebar" }),
).toBeVisible(); ).toBeVisible();
@@ -1014,7 +1014,7 @@ export const OpensSettingsForNonAdmins: Story = {
}); });
expect( expect(
screen.queryByRole("link", { name: "Manage Agents" }), screen.queryByRole("link", { name: "Manage agents" }),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}, },
}; };
@@ -1032,7 +1032,7 @@ export const OpensAdminSubPanelOnMobile: Story = {
}, },
play: async () => { play: async () => {
await userEvent.click( await userEvent.click(
await screen.findByRole("link", { name: "Manage Agents" }), await screen.findByRole("link", { name: "Manage agents" }),
); );
await expect( await expect(
@@ -1059,7 +1059,7 @@ export const SettingsViewResets: Story = {
}); });
// Navigate to the admin panel, then open the Spend section. // Navigate to the admin panel, then open the Spend section.
await userEvent.click(screen.getByRole("link", { name: "Manage Agents" })); await userEvent.click(screen.getByRole("link", { name: "Manage agents" }));
await userEvent.click(await screen.findByRole("link", { name: "Spend" })); await userEvent.click(await screen.findByRole("link", { name: "Spend" }));
await waitFor(() => { await waitFor(() => {
expect( expect(
@@ -1071,11 +1071,11 @@ export const SettingsViewResets: Story = {
// Step back to the top-level settings panel, then back to conversations. // Step back to the top-level settings panel, then back to conversations.
const backToSettingsButton = await screen.findByRole("link", { const backToSettingsButton = await screen.findByRole("link", {
name: "Back to Settings", name: "Back to settings",
}); });
await userEvent.click(backToSettingsButton); await userEvent.click(backToSettingsButton);
const backToAgentsButton = await screen.findByRole("link", { const backToAgentsButton = await screen.findByRole("link", {
name: "Back to Agents", name: "Back to agents",
}); });
await userEvent.click(backToAgentsButton); await userEvent.click(backToAgentsButton);
@@ -476,7 +476,7 @@ export const AgentCreateForm: FC<AgentCreateFormProps> = ({
severity="info" severity="info"
actions={ actions={
<Button asChild size="sm"> <Button asChild size="sm">
<Link to="/agents/analytics">View Usage</Link> <Link to="/agents/analytics">View usage</Link>
</Button> </Button>
} }
> >
@@ -112,7 +112,7 @@ export const AutoArchiveSettings: FC<AutoArchiveSettingsProps> = ({
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="m-0 text-sm font-semibold text-content-primary"> <h3 className="m-0 text-sm font-semibold text-content-primary">
Auto-Archive Inactive Conversations Auto-archive inactive conversations
</h3> </h3>
</div> </div>
<Switch <Switch
@@ -76,7 +76,7 @@ export const UsageLimitExceeded: Story = {
/** /**
* Provider quota errors use the standard ChatStatusCallout instead of the * Provider quota errors use the standard ChatStatusCallout instead of the
* "View Usage" CTA (which links to Coder's analytics, not the provider's * "View usage" CTA (which links to Coder's analytics, not the provider's
* billing page). * billing page).
*/ */
export const ProviderQuotaExceeded: Story = { export const ProviderQuotaExceeded: Story = {
@@ -97,7 +97,6 @@ export const ProviderQuotaExceeded: Story = {
expect( expect(
canvas.getByText(/usage quota for openai has been exceeded/i), canvas.getByText(/usage quota for openai has been exceeded/i),
).toBeVisible(); ).toBeVisible();
// The "View Usage" link must NOT appear for provider-originated quota errors.
expect( expect(
canvas.queryByRole("link", { name: /view usage/i }), canvas.queryByRole("link", { name: /view usage/i }),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
@@ -98,7 +98,7 @@ export const LiveStreamTailContent = ({
severity="info" severity="info"
actions={ actions={
<Button asChild size="sm"> <Button asChild size="sm">
<Link to="/agents/analytics">View Usage</Link> <Link to="/agents/analytics">View usage</Link>
</Button> </Button>
} }
> >
@@ -174,7 +174,7 @@ export const ChatCostSummaryView: FC<ChatCostSummaryViewProps> = ({
</div> </div>
<div className="rounded-lg border border-border-default bg-surface-secondary p-4"> <div className="rounded-lg border border-border-default bg-surface-secondary p-4">
<p className="text-xs font-medium uppercase tracking-wide text-content-secondary"> <p className="text-xs font-medium uppercase tracking-wide text-content-secondary">
Cache Read Cache read
</p> </p>
<p className="mt-1 text-2xl font-semibold text-content-primary"> <p className="mt-1 text-2xl font-semibold text-content-primary">
{formatTokenCount(summary.total_cache_read_tokens)} {formatTokenCount(summary.total_cache_read_tokens)}
@@ -182,7 +182,7 @@ export const ChatCostSummaryView: FC<ChatCostSummaryViewProps> = ({
</div> </div>
<div className="rounded-lg border border-border-default bg-surface-secondary p-4"> <div className="rounded-lg border border-border-default bg-surface-secondary p-4">
<p className="text-xs font-medium uppercase tracking-wide text-content-secondary"> <p className="text-xs font-medium uppercase tracking-wide text-content-secondary">
Cache Write Cache write
</p> </p>
<p className="mt-1 text-2xl font-semibold text-content-primary"> <p className="mt-1 text-2xl font-semibold text-content-primary">
{formatTokenCount(summary.total_cache_creation_tokens)} {formatTokenCount(summary.total_cache_creation_tokens)}
@@ -206,7 +206,7 @@ export const ChatCostSummaryView: FC<ChatCostSummaryViewProps> = ({
<div className="flex flex-col gap-2 md:flex-row md:items-end md:justify-between"> <div className="flex flex-col gap-2 md:flex-row md:items-end md:justify-between">
<div> <div>
<p className="text-xs font-medium uppercase tracking-wide text-content-secondary"> <p className="text-xs font-medium uppercase tracking-wide text-content-secondary">
{usageLimitPeriodLabel} Spend Limit {usageLimitPeriodLabel} spend limit
</p> </p>
{usageLimitCurrentPeriod && ( {usageLimitCurrentPeriod && (
<p className="mt-1 text-sm text-content-secondary"> <p className="mt-1 text-sm text-content-secondary">
@@ -288,8 +288,8 @@ export const ChatCostSummaryView: FC<ChatCostSummaryViewProps> = ({
<TableHead className="text-right">Messages</TableHead> <TableHead className="text-right">Messages</TableHead>
<TableHead className="text-right">Input</TableHead> <TableHead className="text-right">Input</TableHead>
<TableHead className="text-right">Output</TableHead> <TableHead className="text-right">Output</TableHead>
<TableHead className="text-right">Cache Read</TableHead> <TableHead className="text-right">Cache read</TableHead>
<TableHead className="text-right">Cache Write</TableHead> <TableHead className="text-right">Cache write</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@@ -344,8 +344,8 @@ export const ChatCostSummaryView: FC<ChatCostSummaryViewProps> = ({
<TableHead className="text-right">Messages</TableHead> <TableHead className="text-right">Messages</TableHead>
<TableHead className="text-right">Input</TableHead> <TableHead className="text-right">Input</TableHead>
<TableHead className="text-right">Output</TableHead> <TableHead className="text-right">Output</TableHead>
<TableHead className="text-right">Cache Read</TableHead> <TableHead className="text-right">Cache read</TableHead>
<TableHead className="text-right">Cache Write</TableHead> <TableHead className="text-right">Cache write</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
@@ -8,7 +8,7 @@ export const ChatFullWidthSettings: FC = () => {
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h3 className="m-0 text-sm font-semibold text-content-primary"> <h3 className="m-0 text-sm font-semibold text-content-primary">
Chat Layout Chat layout
</h3> </h3>
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<p className="m-0 flex-1 text-xs text-content-secondary"> <p className="m-0 flex-1 text-xs text-content-secondary">
@@ -965,8 +965,7 @@ export const SubmitModelConfigExplicitly: Story = {
await body.findByLabelText(/Max output tokens/i), await body.findByLabelText(/Max output tokens/i),
"32000", "32000",
); );
// Reasoning Effort is a provider option under "Provider Configuration". await expandSection(body, "Provider configuration");
await expandSection(body, "Provider Configuration");
const effortGroup = await body.findByRole("radiogroup", { const effortGroup = await body.findByRole("radiogroup", {
name: "Reasoning Effort", name: "Reasoning Effort",
}); });
@@ -1192,7 +1191,7 @@ const ensureCostTrackingOpen = async (body: ReturnType<typeof within>) => {
if (body.queryByLabelText(/^Input$/i)) { if (body.queryByLabelText(/^Input$/i)) {
return; return;
} }
await expandSection(body, "Cost Tracking"); await expandSection(body, "Cost tracking");
await body.findByLabelText(/^Input$/i); await body.findByLabelText(/^Input$/i);
}; };
@@ -1271,7 +1270,7 @@ const ensureProviderConfigurationOpen = async (
if (body.queryByLabelText(/Max Completion Tokens/i)) { if (body.queryByLabelText(/Max Completion Tokens/i)) {
return; return;
} }
await expandSection(body, "Provider Configuration"); await expandSection(body, "Provider configuration");
await body.findByLabelText(/Max Completion Tokens/i); await body.findByLabelText(/Max Completion Tokens/i);
}; };
@@ -1321,7 +1320,7 @@ export const OpenAIKnownModelHappyPath: Story = {
); );
await expect(body.getByLabelText(/Context limit/i)).toHaveValue("1050000"); await expect(body.getByLabelText(/Context limit/i)).toHaveValue("1050000");
await expandSection(body, "Provider Configuration"); await expandSection(body, "Provider configuration");
await expect( await expect(
await body.findByLabelText(/Max Completion Tokens/i), await body.findByLabelText(/Max Completion Tokens/i),
).toHaveValue("128000"); ).toHaveValue("128000");
@@ -1384,7 +1383,7 @@ export const AnthropicKnownModelHappyPath: Story = {
"128000", "128000",
); );
await expandSection(body, "Provider Configuration"); await expandSection(body, "Provider configuration");
const sendReasoningGroup = await body.findByRole("radiogroup", { const sendReasoningGroup = await body.findByRole("radiogroup", {
name: "Send Reasoning", name: "Send Reasoning",
}); });
@@ -1409,7 +1408,7 @@ export const AnthropicHaikuKnownModelUsesThinkingBudgetNotEffort: Story = {
await openAddModelForm(body, "Anthropic"); await openAddModelForm(body, "Anthropic");
await selectKnownModel(body, "claude-haiku-4-5"); await selectKnownModel(body, "claude-haiku-4-5");
await expandSection(body, "Provider Configuration"); await expandSection(body, "Provider configuration");
// Reasoning Effort should remain empty because Haiku 4.5 uses the // Reasoning Effort should remain empty because Haiku 4.5 uses the
// thinking budget path instead of Anthropic adaptive thinking. // thinking budget path instead of Anthropic adaptive thinking.
@@ -1981,7 +1980,7 @@ export const ModelFormOpenAI: Story = {
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body); const body = within(canvasElement.ownerDocument.body);
await openAddModelForm(body, "OpenAI"); await openAddModelForm(body, "OpenAI");
await expandSection(body, "Provider Configuration"); await expandSection(body, "Provider configuration");
await expect( await expect(
await body.findByLabelText(/Reasoning Effort/i), await body.findByLabelText(/Reasoning Effort/i),
).toBeInTheDocument(); ).toBeInTheDocument();
@@ -1996,7 +1995,7 @@ export const ModelFormAnthropic: Story = {
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body); const body = within(canvasElement.ownerDocument.body);
await openAddModelForm(body, "Anthropic"); await openAddModelForm(body, "Anthropic");
await expandSection(body, "Provider Configuration"); await expandSection(body, "Provider configuration");
await expect( await expect(
await body.findByLabelText(/Send Reasoning/i), await body.findByLabelText(/Send Reasoning/i),
).toBeInTheDocument(); ).toBeInTheDocument();
@@ -2011,7 +2010,7 @@ export const ModelFormGoogle: Story = {
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body); const body = within(canvasElement.ownerDocument.body);
await openAddModelForm(body, "Google"); await openAddModelForm(body, "Google");
await expandSection(body, "Provider Configuration"); await expandSection(body, "Provider configuration");
await expect( await expect(
await body.findByLabelText(/Thinking Config Thinking Budget/i), await body.findByLabelText(/Thinking Config Thinking Budget/i),
).toBeInTheDocument(); ).toBeInTheDocument();
@@ -2026,7 +2025,7 @@ export const ModelFormOpenAICompat: Story = {
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body); const body = within(canvasElement.ownerDocument.body);
await openAddModelForm(body, "OpenAI-compatible"); await openAddModelForm(body, "OpenAI-compatible");
await expandSection(body, "Provider Configuration"); await expandSection(body, "Provider configuration");
await expect( await expect(
await body.findByLabelText(/Reasoning Effort/i), await body.findByLabelText(/Reasoning Effort/i),
).toBeInTheDocument(); ).toBeInTheDocument();
@@ -2038,7 +2037,7 @@ export const ModelFormOpenRouter: Story = {
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body); const body = within(canvasElement.ownerDocument.body);
await openAddModelForm(body, "OpenRouter"); await openAddModelForm(body, "OpenRouter");
await expandSection(body, "Provider Configuration"); await expandSection(body, "Provider configuration");
await expect( await expect(
await body.findByLabelText(/Reasoning Enabled/i), await body.findByLabelText(/Reasoning Enabled/i),
).toBeInTheDocument(); ).toBeInTheDocument();
@@ -2053,7 +2052,7 @@ export const ModelFormVercel: Story = {
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body); const body = within(canvasElement.ownerDocument.body);
await openAddModelForm(body, "Vercel AI Gateway"); await openAddModelForm(body, "Vercel AI Gateway");
await expandSection(body, "Provider Configuration"); await expandSection(body, "Provider configuration");
await expect( await expect(
await body.findByLabelText(/Reasoning Enabled/i), await body.findByLabelText(/Reasoning Enabled/i),
).toBeInTheDocument(); ).toBeInTheDocument();
@@ -2068,7 +2067,7 @@ export const ModelFormAzure: Story = {
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body); const body = within(canvasElement.ownerDocument.body);
await openAddModelForm(body, "Azure OpenAI"); await openAddModelForm(body, "Azure OpenAI");
await expandSection(body, "Provider Configuration"); await expandSection(body, "Provider configuration");
// Azure aliases to OpenAI fields. // Azure aliases to OpenAI fields.
await expect( await expect(
await body.findByLabelText(/Reasoning Effort/i), await body.findByLabelText(/Reasoning Effort/i),
@@ -2084,7 +2083,7 @@ export const ModelFormBedrock: Story = {
play: async ({ canvasElement }) => { play: async ({ canvasElement }) => {
const body = within(canvasElement.ownerDocument.body); const body = within(canvasElement.ownerDocument.body);
await openAddModelForm(body, "AWS Bedrock"); await openAddModelForm(body, "AWS Bedrock");
await expandSection(body, "Provider Configuration"); await expandSection(body, "Provider configuration");
// Bedrock aliases to Anthropic fields. // Bedrock aliases to Anthropic fields.
await expect( await expect(
await body.findByLabelText(/Send Reasoning/i), await body.findByLabelText(/Send Reasoning/i),
@@ -49,8 +49,8 @@ const unsetSelectValue = "__unset__";
const shortLabelOverrides: Record<string, string> = { const shortLabelOverrides: Record<string, string> = {
"cost.input_price_per_million_tokens": "Input", "cost.input_price_per_million_tokens": "Input",
"cost.output_price_per_million_tokens": "Output", "cost.output_price_per_million_tokens": "Output",
"cost.cache_read_price_per_million_tokens": "Cache Read", "cost.cache_read_price_per_million_tokens": "Cache read",
"cost.cache_write_price_per_million_tokens": "Cache Write", "cost.cache_write_price_per_million_tokens": "Cache write",
}; };
/** /**
@@ -99,8 +99,8 @@ function snakeToPrettyLabel(field: FieldSchema): string {
if (shortLabelOverrides[field.json_name]) { if (shortLabelOverrides[field.json_name]) {
return shortLabelOverrides[field.json_name]; return shortLabelOverrides[field.json_name];
} }
return field.json_name const words = field.json_name.split(/[._]/);
.split(/[._]/) return words
.map((word) => word.charAt(0).toUpperCase() + word.slice(1)) .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" "); .join(" ");
} }
@@ -129,10 +129,10 @@ export const ModelForm: FC<ModelFormProps> = ({
selectedProviderState.providerConfig.allow_user_api_key), selectedProviderState.providerConfig.allow_user_api_key),
); );
const formTitle = isEditing const formTitle = isEditing
? "Edit Model" ? "Edit model"
: isDuplicating : isDuplicating
? "Duplicate Model" ? "Duplicate model"
: "Add Model"; : "Add model";
const formDescription = isDuplicating const formDescription = isDuplicating
? "Review the copied settings, then save to create a new model." ? "Review the copied settings, then save to create a new model."
: undefined; : undefined;
@@ -403,7 +403,7 @@ export const ModelForm: FC<ModelFormProps> = ({
autoComplete="off" autoComplete="off"
> >
<div className="space-y-6"> <div className="space-y-6">
{/* Model ID + Context Limit + Pricing */} {/* Model ID + Context limit + Pricing */}
<div className="space-y-4"> <div className="space-y-4">
<div className="grid items-start gap-4 sm:grid-cols-2"> <div className="grid items-start gap-4 sm:grid-cols-2">
{" "} {" "}
@@ -419,7 +419,7 @@ export const ModelForm: FC<ModelFormProps> = ({
htmlFor={contextLimitField.id} htmlFor={contextLimitField.id}
className="inline-flex items-center gap-1 text-sm font-medium text-content-primary" className="inline-flex items-center gap-1 text-sm font-medium text-content-primary"
> >
Context Limit{" "} Context limit{" "}
<span className="text-xs font-bold text-content-destructive"> <span className="text-xs font-bold text-content-destructive">
* *
</span> </span>
@@ -464,7 +464,7 @@ export const ModelForm: FC<ModelFormProps> = ({
</div> </div>
</div> </div>
{/* Usage Tracking */} {/* Cost tracking */}
<div className="border-0 border-t border-solid border-border pt-4"> <div className="border-0 border-t border-solid border-border pt-4">
<button <button
type="button" type="button"
@@ -473,7 +473,7 @@ export const ModelForm: FC<ModelFormProps> = ({
> >
<div> <div>
<h3 className="m-0 text-sm font-medium text-content-primary"> <h3 className="m-0 text-sm font-medium text-content-primary">
Cost Tracking{" "} Cost tracking
</h3> </h3>
<p className="m-0 text-xs text-content-secondary"> <p className="m-0 text-xs text-content-secondary">
Set per-token pricing so Coder can track costs and enforce Set per-token pricing so Coder can track costs and enforce
@@ -498,7 +498,7 @@ export const ModelForm: FC<ModelFormProps> = ({
)} )}
</div> </div>
{/* Provider Configuration */} {/* Provider configuration */}
<div className="border-0 border-t border-solid border-border pt-4"> <div className="border-0 border-t border-solid border-border pt-4">
<button <button
type="button" type="button"
@@ -507,7 +507,7 @@ export const ModelForm: FC<ModelFormProps> = ({
> >
<div> <div>
<h3 className="m-0 text-sm font-medium text-content-primary"> <h3 className="m-0 text-sm font-medium text-content-primary">
Provider Configuration Provider configuration
</h3> </h3>
<p className="m-0 text-xs text-content-secondary"> <p className="m-0 text-xs text-content-secondary">
Tune provider-specific behavior like reasoning, tool calling, Tune provider-specific behavior like reasoning, tool calling,
@@ -567,7 +567,7 @@ export const ModelForm: FC<ModelFormProps> = ({
htmlFor={compressionThresholdField.id} htmlFor={compressionThresholdField.id}
className="inline-flex items-center gap-1 text-[13px] font-medium text-content-primary" className="inline-flex items-center gap-1 text-[13px] font-medium text-content-primary"
> >
Compression Threshold Compression threshold
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<InfoIcon className="size-3 text-content-secondary" /> <InfoIcon className="size-3 text-content-secondary" />
@@ -459,7 +459,7 @@ export const ModelIdentifierField = ({
htmlFor={modelField.id} htmlFor={modelField.id}
className="inline-flex items-center gap-1 text-sm font-medium text-content-primary" className="inline-flex items-center gap-1 text-sm font-medium text-content-primary"
> >
Model Identifier{" "} Model identifier{" "}
<span className="text-xs font-bold text-content-destructive">*</span> <span className="text-xs font-bold text-content-destructive">*</span>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
@@ -213,23 +213,23 @@ export const OpensDuplicateFormWithoutCreating: Story = {
}), }),
); );
await expect(canvas.findByText("Duplicate Model")).resolves.toBeVisible(); await expect(canvas.findByText("Duplicate model")).resolves.toBeVisible();
expect(args.onCreateModel).not.toHaveBeenCalled(); expect(args.onCreateModel).not.toHaveBeenCalled();
expect(args.onUpdateModel).not.toHaveBeenCalled(); expect(args.onUpdateModel).not.toHaveBeenCalled();
expect(canvas.getByDisplayValue("GPT-4.1 Default")).toBeVisible(); expect(canvas.getByDisplayValue("GPT-4.1 Default")).toBeVisible();
expect(canvas.getByLabelText(/Model Identifier/)).toHaveValue( expect(canvas.getByLabelText(/Model identifier/)).toHaveValue(
"gpt-4.1-default", "gpt-4.1-default",
); );
expect(canvas.getByLabelText(/Context Limit/)).toHaveValue("200000"); expect(canvas.getByLabelText(/Context limit/)).toHaveValue("200000");
const enabledSwitch = canvas.getByRole("switch", { name: "Enabled" }); const enabledSwitch = canvas.getByRole("switch", { name: "Enabled" });
expect(enabledSwitch).toBeChecked(); expect(enabledSwitch).toBeChecked();
expect(enabledSwitch).toBeEnabled(); expect(enabledSwitch).toBeEnabled();
await userEvent.click(canvas.getByRole("button", { name: /Advanced/ })); await userEvent.click(canvas.getByRole("button", { name: /Advanced/ }));
expect(canvas.getByLabelText(/Compression Threshold/)).toHaveValue("65"); expect(canvas.getByLabelText(/Compression threshold/)).toHaveValue("65");
await userEvent.click( await userEvent.click(
canvas.getByRole("button", { name: /Provider Configuration/ }), canvas.getByRole("button", { name: /Provider configuration/ }),
); );
expect(canvas.getByLabelText("Max Tool Calls")).toHaveValue("4"); expect(canvas.getByLabelText("Max Tool Calls")).toHaveValue("4");
}, },
@@ -246,14 +246,14 @@ export const AbandonsDuplicateWithoutSaving: Story = {
const copyButtonName = "Duplicate model: GPT-4.1 Default"; const copyButtonName = "Duplicate model: GPT-4.1 Default";
await userEvent.click(canvas.getByRole("button", { name: copyButtonName })); await userEvent.click(canvas.getByRole("button", { name: copyButtonName }));
await expect(canvas.findByText("Duplicate Model")).resolves.toBeVisible(); await expect(canvas.findByText("Duplicate model")).resolves.toBeVisible();
await userEvent.click(canvas.getByRole("button", { name: "Cancel" })); await userEvent.click(canvas.getByRole("button", { name: "Cancel" }));
await expect( await expect(
canvas.findByRole("button", { name: copyButtonName }), canvas.findByRole("button", { name: copyButtonName }),
).resolves.toBeVisible(); ).resolves.toBeVisible();
await userEvent.click(canvas.getByRole("button", { name: copyButtonName })); await userEvent.click(canvas.getByRole("button", { name: copyButtonName }));
await expect(canvas.findByText("Duplicate Model")).resolves.toBeVisible(); await expect(canvas.findByText("Duplicate model")).resolves.toBeVisible();
await userEvent.click(canvas.getByRole("button", { name: "Back" })); await userEvent.click(canvas.getByRole("button", { name: "Back" }));
await expect( await expect(
canvas.findByRole("button", { name: copyButtonName }), canvas.findByRole("button", { name: copyButtonName }),
@@ -276,9 +276,9 @@ export const SavesDuplicateAsCreateRequest: Story = {
name: "Duplicate model: GPT-4.1 Default", name: "Duplicate model: GPT-4.1 Default",
}), }),
); );
await expect(canvas.findByText("Duplicate Model")).resolves.toBeVisible(); await expect(canvas.findByText("Duplicate model")).resolves.toBeVisible();
const modelInput = canvas.getByLabelText(/Model Identifier/); const modelInput = canvas.getByLabelText(/Model identifier/);
await userEvent.clear(modelInput); await userEvent.clear(modelInput);
await userEvent.type(modelInput, "gpt-4.1-copy"); await userEvent.type(modelInput, "gpt-4.1-copy");
const displayNameInput = canvas.getByDisplayValue("GPT-4.1 Default"); const displayNameInput = canvas.getByDisplayValue("GPT-4.1 Default");
@@ -329,14 +329,14 @@ export const SavesNonDefaultDuplicateWithEditableEnabled: Story = {
await userEvent.click( await userEvent.click(
canvas.getByRole("button", { name: "Duplicate model: GPT-4.1" }), canvas.getByRole("button", { name: "Duplicate model: GPT-4.1" }),
); );
await expect(canvas.findByText("Duplicate Model")).resolves.toBeVisible(); await expect(canvas.findByText("Duplicate model")).resolves.toBeVisible();
const enabledSwitch = canvas.getByRole("switch", { name: "Enabled" }); const enabledSwitch = canvas.getByRole("switch", { name: "Enabled" });
expect(enabledSwitch).toBeChecked(); expect(enabledSwitch).toBeChecked();
expect(enabledSwitch).toBeEnabled(); expect(enabledSwitch).toBeEnabled();
await userEvent.click(enabledSwitch); await userEvent.click(enabledSwitch);
const modelInput = canvas.getByLabelText(/Model Identifier/); const modelInput = canvas.getByLabelText(/Model identifier/);
await userEvent.clear(modelInput); await userEvent.clear(modelInput);
await userEvent.type(modelInput, "gpt-4.1-copy"); await userEvent.type(modelInput, "gpt-4.1-copy");
await userEvent.click( await userEvent.click(
@@ -373,14 +373,14 @@ export const SavesDisabledDuplicateWithEditableEnabled: Story = {
name: "Duplicate model: GPT-4.1 Disabled", name: "Duplicate model: GPT-4.1 Disabled",
}), }),
); );
await expect(canvas.findByText("Duplicate Model")).resolves.toBeVisible(); await expect(canvas.findByText("Duplicate model")).resolves.toBeVisible();
const enabledSwitch = canvas.getByRole("switch", { name: "Enabled" }); const enabledSwitch = canvas.getByRole("switch", { name: "Enabled" });
expect(enabledSwitch).not.toBeChecked(); expect(enabledSwitch).not.toBeChecked();
expect(enabledSwitch).toBeEnabled(); expect(enabledSwitch).toBeEnabled();
await userEvent.click(enabledSwitch); await userEvent.click(enabledSwitch);
const modelInput = canvas.getByLabelText(/Model Identifier/); const modelInput = canvas.getByLabelText(/Model identifier/);
await userEvent.clear(modelInput); await userEvent.clear(modelInput);
await userEvent.type(modelInput, "gpt-4.1-disabled-copy"); await userEvent.type(modelInput, "gpt-4.1-disabled-copy");
await userEvent.click( await userEvent.click(
@@ -417,7 +417,7 @@ export const DisablesDuplicateWhenProviderCannotManageModels: Story = {
expect(duplicateButton).toHaveAttribute("aria-disabled", "true"); expect(duplicateButton).toHaveAttribute("aria-disabled", "true");
await userEvent.click(duplicateButton); await userEvent.click(duplicateButton);
expect(canvas.queryByText("Duplicate Model")).not.toBeInTheDocument(); expect(canvas.queryByText("Duplicate model")).not.toBeInTheDocument();
expect(args.onCreateModel).not.toHaveBeenCalled(); expect(args.onCreateModel).not.toHaveBeenCalled();
}, },
}; };
@@ -442,13 +442,13 @@ export const RowActionsDoNotOpenRowBody: Story = {
"model-config-id", "model-config-id",
{ is_default: true }, { is_default: true },
]); ]);
expect(canvas.queryByText("Edit Model")).not.toBeInTheDocument(); expect(canvas.queryByText("Edit model")).not.toBeInTheDocument();
await userEvent.click( await userEvent.click(
canvas.getByRole("button", { name: "Default model: GPT-4o" }), canvas.getByRole("button", { name: "Default model: GPT-4o" }),
); );
expect(args.onUpdateModel).toHaveBeenCalledTimes(1); expect(args.onUpdateModel).toHaveBeenCalledTimes(1);
expect(canvas.queryByText("Edit Model")).not.toBeInTheDocument(); expect(canvas.queryByText("Edit model")).not.toBeInTheDocument();
const disabledStarButton = canvas.getByRole("button", { const disabledStarButton = canvas.getByRole("button", {
name: "Set as default model: GPT-4.1 Disabled", name: "Set as default model: GPT-4.1 Disabled",
@@ -456,20 +456,20 @@ export const RowActionsDoNotOpenRowBody: Story = {
expect(disabledStarButton).toHaveAttribute("aria-disabled", "true"); expect(disabledStarButton).toHaveAttribute("aria-disabled", "true");
await userEvent.click(disabledStarButton); await userEvent.click(disabledStarButton);
expect(args.onUpdateModel).toHaveBeenCalledTimes(1); expect(args.onUpdateModel).toHaveBeenCalledTimes(1);
expect(canvas.queryByText("Edit Model")).not.toBeInTheDocument(); expect(canvas.queryByText("Edit model")).not.toBeInTheDocument();
await userEvent.click( await userEvent.click(
canvas.getByRole("button", { name: "Duplicate model: GPT-4.1" }), canvas.getByRole("button", { name: "Duplicate model: GPT-4.1" }),
); );
await expect(canvas.findByText("Duplicate Model")).resolves.toBeVisible(); await expect(canvas.findByText("Duplicate model")).resolves.toBeVisible();
expect(args.onCreateModel).not.toHaveBeenCalled(); expect(args.onCreateModel).not.toHaveBeenCalled();
expect(args.onUpdateModel).toHaveBeenCalledTimes(1); expect(args.onUpdateModel).toHaveBeenCalledTimes(1);
expect(canvas.queryByText("Edit Model")).not.toBeInTheDocument(); expect(canvas.queryByText("Edit model")).not.toBeInTheDocument();
await userEvent.click(canvas.getByRole("button", { name: "Back" })); await userEvent.click(canvas.getByRole("button", { name: "Back" }));
await userEvent.click( await userEvent.click(
await canvas.findByRole("button", { name: "Edit model: GPT-4.1" }), await canvas.findByRole("button", { name: "Edit model: GPT-4.1" }),
); );
await expect(canvas.findByText("Edit Model")).resolves.toBeVisible(); await expect(canvas.findByText("Edit model")).resolves.toBeVisible();
}, },
}; };
@@ -22,7 +22,7 @@ export const ChatSendShortcutSettings: FC = () => {
return ( return (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h3 className="m-0 text-sm font-semibold text-content-primary"> <h3 className="m-0 text-sm font-semibold text-content-primary">
Keyboard Shortcuts Keyboard shortcuts
</h3> </h3>
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<p <p
@@ -2117,7 +2117,7 @@ export const SettingsUserAgentsNonAdmin: Story = {
const agentsLink = canvas.getByRole("link", { name: "Agents" }); const agentsLink = canvas.getByRole("link", { name: "Agents" });
await expect(agentsLink).toHaveAttribute("aria-current", "page"); await expect(agentsLink).toHaveAttribute("aria-current", "page");
expect( expect(
canvas.queryByRole("link", { name: "Manage Agents" }), canvas.queryByRole("link", { name: "Manage agents" }),
).not.toBeInTheDocument(); ).not.toBeInTheDocument();
}, },
}; };
@@ -2192,7 +2192,7 @@ export const SettingsUserAgentsAdmin: Story = {
const agentsLink = canvas.getByRole("link", { name: "Agents" }); const agentsLink = canvas.getByRole("link", { name: "Agents" });
await expect(agentsLink).toHaveAttribute("aria-current", "page"); await expect(agentsLink).toHaveAttribute("aria-current", "page");
expect( expect(
canvas.getByRole("link", { name: "Manage Agents" }), canvas.getByRole("link", { name: "Manage agents" }),
).toBeInTheDocument(); ).toBeInTheDocument();
}, },
}; };
@@ -2212,7 +2212,7 @@ export const SettingsAdminAgentsEntryPreserved: Story = {
const canvas = within(canvasElement); const canvas = within(canvasElement);
const agentsLink = canvas.getByRole("link", { name: "Agents" }); const agentsLink = canvas.getByRole("link", { name: "Agents" });
await expect(agentsLink).toHaveAttribute("aria-current", "page"); await expect(agentsLink).toHaveAttribute("aria-current", "page");
expect(canvas.getByText("Manage Agents")).toBeInTheDocument(); expect(canvas.getByText("Manage agents")).toBeInTheDocument();
}, },
}; };
@@ -389,7 +389,7 @@ export const ChatsPanel: FC<ChatsPanelProps> = ({
</div> </div>
<SettingsNavItem <SettingsNavItem
icon={SquarePenIcon} icon={SquarePenIcon}
label="New Agent" label="New chat"
active={isChatsActive} active={isChatsActive}
to={{ pathname: "/agents", search: locationSearch }} to={{ pathname: "/agents", search: locationSearch }}
onClick={onBeforeNewAgent} onClick={onBeforeNewAgent}
@@ -45,7 +45,7 @@ export const SettingsPanel: FC<SettingsPanelProps> = ({
onCollapse, onCollapse,
}) => { }) => {
const subNavTitle = const subNavTitle =
settingsPanel === "settings-admin" ? "Manage Agents" : "Settings"; settingsPanel === "settings-admin" ? "Manage agents" : "Settings";
return ( return (
<div <div
@@ -67,8 +67,8 @@ export const SettingsPanel: FC<SettingsPanelProps> = ({
size="icon" size="icon"
aria-label={ aria-label={
settingsPanel === "settings-admin" settingsPanel === "settings-admin"
? "Back to Settings" ? "Back to settings"
: "Back to Agents" : "Back to agents"
} }
className="relative z-10 size-7 min-w-0 text-content-secondary hover:text-content-primary" className="relative z-10 size-7 min-w-0 text-content-secondary hover:text-content-primary"
> >
@@ -76,7 +76,7 @@ export const SettingsPanel: FC<SettingsPanelProps> = ({
<Link <Link
to="/agents/settings/general" to="/agents/settings/general"
state={location.state} state={location.state}
aria-label="Back to Settings" aria-label="Back to settings"
> >
<ArrowLeftIcon /> <ArrowLeftIcon />
</Link> </Link>
@@ -122,7 +122,7 @@ export const SettingsPanel: FC<SettingsPanelProps> = ({
)} )}
<SettingsNavItem <SettingsNavItem
icon={ReceiptTextIcon} icon={ReceiptTextIcon}
label="Personal Skills" label="Personal skills"
active={settingsSection === "personal-skills"} active={settingsSection === "personal-skills"}
to="/agents/settings/personal-skills" to="/agents/settings/personal-skills"
state={location.state} state={location.state}
@@ -146,7 +146,7 @@ export const SettingsPanel: FC<SettingsPanelProps> = ({
{isAdmin && ( {isAdmin && (
<SettingsNavItem <SettingsNavItem
icon={Settings2Icon} icon={Settings2Icon}
label="Manage Agents" label="Manage agents"
active={false} active={false}
to="/agents/settings/admin" to="/agents/settings/admin"
state={location.state} state={location.state}
@@ -179,7 +179,7 @@ export const SettingsPanel: FC<SettingsPanelProps> = ({
/> />
<SettingsNavItem <SettingsNavItem
icon={ServerIcon} icon={ServerIcon}
label="MCP Servers" label="MCP servers"
active={settingsSection === "mcp-servers"} active={settingsSection === "mcp-servers"}
to="/agents/settings/mcp-servers" to="/agents/settings/mcp-servers"
state={location.state} state={location.state}
@@ -114,7 +114,7 @@ export const DebugRetentionSettings: FC<DebugRetentionSettingsProps> = ({
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="m-0 text-sm font-semibold text-content-primary"> <h3 className="m-0 text-sm font-semibold text-content-primary">
Chat Debug Data Retention Chat debug data retention
</h3> </h3>
</div> </div>
<Switch <Switch
@@ -24,14 +24,14 @@ type AgentDisplayMode = UserPreferenceSettings["code_diff_display_mode"];
const thinkingDisplayOptions: DisplayModeOption<ThinkingDisplayMode>[] = [ const thinkingDisplayOptions: DisplayModeOption<ThinkingDisplayMode>[] = [
{ value: "auto", label: "Auto" }, { value: "auto", label: "Auto" },
{ value: "preview", label: "Preview" }, { value: "preview", label: "Preview" },
{ value: "always_expanded", label: "Always Expanded" }, { value: "always_expanded", label: "Always expanded" },
{ value: "always_collapsed", label: "Always Collapsed" }, { value: "always_collapsed", label: "Always collapsed" },
]; ];
const agentDisplayOptions: DisplayModeOption<AgentDisplayMode>[] = [ const agentDisplayOptions: DisplayModeOption<AgentDisplayMode>[] = [
{ value: "auto", label: "Auto" }, { value: "auto", label: "Auto" },
{ value: "always_expanded", label: "Always Expanded" }, { value: "always_expanded", label: "Always expanded" },
{ value: "always_collapsed", label: "Always Collapsed" }, { value: "always_collapsed", label: "Always collapsed" },
]; ];
type DisplayModeSettingsProps<T extends string> = { type DisplayModeSettingsProps<T extends string> = {
@@ -101,8 +101,8 @@ const DisplayModeSettings = <T extends string>({
export const ThinkingDisplaySettings: FC = () => { export const ThinkingDisplaySettings: FC = () => {
return ( return (
<DisplayModeSettings <DisplayModeSettings
title="Thinking Display" title="Thinking display"
description="How thinking blocks should be displayed by default. 'Auto' fully expands during streaming, then auto-collapses when done. 'Preview' auto-expands with a height constraint during streaming. 'Always Expanded' shows full content. 'Always Collapsed' keeps them collapsed." description="How thinking blocks should be displayed by default. 'Auto' fully expands during streaming, then auto-collapses when done. 'Preview' auto-expands with a height constraint during streaming. 'Always expanded' shows full content. 'Always collapsed' keeps them collapsed."
ariaLabel="Thinking display mode" ariaLabel="Thinking display mode"
errorMessage="Failed to save your thinking display preference." errorMessage="Failed to save your thinking display preference."
defaultValue="auto" defaultValue="auto"
@@ -118,8 +118,8 @@ export const ThinkingDisplaySettings: FC = () => {
export const ShellToolDisplaySettings: FC = () => { export const ShellToolDisplaySettings: FC = () => {
return ( return (
<DisplayModeSettings <DisplayModeSettings
title="Shell Output Display" title="Shell output display"
description="How shell command output should be displayed by default. 'Auto' opens running commands and completed commands with output, then keeps empty output collapsed. 'Always Expanded' opens shell output by default. 'Always Collapsed' keeps it collapsed." description="How shell command output should be displayed by default. 'Auto' opens running commands and completed commands with output, then keeps empty output collapsed. 'Always expanded' opens shell output by default. 'Always collapsed' keeps it collapsed."
ariaLabel="Shell output display mode" ariaLabel="Shell output display mode"
errorMessage="Failed to save your shell output display preference." errorMessage="Failed to save your shell output display preference."
defaultValue="auto" defaultValue="auto"
@@ -135,8 +135,8 @@ export const ShellToolDisplaySettings: FC = () => {
export const CodeDiffDisplaySettings: FC = () => { export const CodeDiffDisplaySettings: FC = () => {
return ( return (
<DisplayModeSettings <DisplayModeSettings
title="Code Diff Display" title="Code diff display"
description="Controls how code edit diffs appear. Auto starts single-file writes collapsed and opens multi-file edits with a height-constrained preview. Always Expanded opens diffs by default; Always Collapsed keeps them collapsed." description="Controls how code edit diffs appear. 'Auto' starts single-file writes collapsed and opens multi-file edits with a height-constrained preview. 'Always expanded' opens diffs by default; 'Always collapsed' keeps them collapsed."
ariaLabel="Code diff display mode" ariaLabel="Code diff display mode"
errorMessage="Failed to save your code diff display preference." errorMessage="Failed to save your code diff display preference."
defaultValue="auto" defaultValue="auto"
@@ -49,7 +49,7 @@ export const DefaultLimitSection: FC<DefaultLimitSectionProps> = ({
<section className="space-y-4"> <section className="space-y-4">
{!hideHeader && ( {!hideHeader && (
<SectionHeader <SectionHeader
label="Default Spend Limit" label="Default spend limit"
description="Set a deployment-wide spend cap that applies to all users by default." description="Set a deployment-wide spend cap that applies to all users by default."
badge={adminBadge} badge={adminBadge}
/> />
@@ -92,7 +92,7 @@ export const GroupLimitsSection: FC<GroupLimitsSectionProps> = ({
<section className="space-y-4"> <section className="space-y-4">
{!hideHeader && ( {!hideHeader && (
<SectionHeader <SectionHeader
label="Group Limits" label="Group limits"
description="Override the default limit for specific groups. When a user belongs to multiple groups, the lowest group limit applies." description="Override the default limit for specific groups. When a user belongs to multiple groups, the lowest group limit applies."
/> />
)} )}
@@ -103,7 +103,7 @@ export const GroupLimitsSection: FC<GroupLimitsSectionProps> = ({
<TableRow> <TableRow>
<TableHead>Group</TableHead> <TableHead>Group</TableHead>
<TableHead>Members</TableHead> <TableHead>Members</TableHead>
<TableHead>Spend Limit</TableHead> <TableHead>Spend limit</TableHead>
<TableHead className="w-[160px]">Actions</TableHead> <TableHead className="w-[160px]">Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -253,7 +253,7 @@ export const GroupLimitsSection: FC<GroupLimitsSectionProps> = ({
)} )}
</div> </div>
<div className="flex-1 space-y-1"> <div className="flex-1 space-y-1">
<Label htmlFor={groupAmountId}>Spend Limit ($)</Label> <Label htmlFor={groupAmountId}>Spend limit ($)</Label>
<Input <Input
id={groupAmountId} id={groupAmountId}
type="number" type="number"
@@ -84,7 +84,7 @@ export const UserOverridesSection: FC<UserOverridesSectionProps> = ({
<section className="space-y-4"> <section className="space-y-4">
{!hideHeader && ( {!hideHeader && (
<SectionHeader <SectionHeader
label="Per-User Overrides" label="Per-user overrides"
description="Override the deployment default spend limit for specific users. User overrides take highest priority, followed by group limits, then the deployment default." description="Override the deployment default spend limit for specific users. User overrides take highest priority, followed by group limits, then the deployment default."
/> />
)} )}
@@ -94,7 +94,7 @@ export const UserOverridesSection: FC<UserOverridesSectionProps> = ({
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>User</TableHead> <TableHead>User</TableHead>
<TableHead>Spend Limit</TableHead> <TableHead>Spend limit</TableHead>
<TableHead className="w-[160px]">Actions</TableHead> <TableHead className="w-[160px]">Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
@@ -190,7 +190,7 @@ export const UserOverridesSection: FC<UserOverridesSectionProps> = ({
)} )}
</div> </div>
<div className="flex-1 space-y-1"> <div className="flex-1 space-y-1">
<Label htmlFor={userOverrideAmountId}>Spend Limit ($)</Label> <Label htmlFor={userOverrideAmountId}>Spend limit ($)</Label>
<Input <Input
id={userOverrideAmountId} id={userOverrideAmountId}
type="number" type="number"
@@ -248,7 +248,7 @@ const ServerList: FC<ServerListProps> = ({
return ( return (
<> <>
<SectionHeader <SectionHeader
label={sectionLabel ?? "MCP Servers"} label={sectionLabel ?? "MCP servers"}
description={ description={
sectionDescription ?? sectionDescription ??
"Configure external MCP servers that provide additional tools for Coder Agents." "Configure external MCP servers that provide additional tools for Coder Agents."
@@ -532,7 +532,7 @@ const ServerForm: FC<ServerFormProps> = ({
}} }}
placeholder="e.g. Sentry" placeholder="e.g. Sentry"
disabled={isDisabled} disabled={isDisabled}
aria-label="Display Name" aria-label="Display name"
/> />
</Field> </Field>
<Field label="Slug" htmlFor={`${formId}-slug`} required> <Field label="Slug" htmlFor={`${formId}-slug`} required>
@@ -698,7 +698,7 @@ const ServerForm: FC<ServerFormProps> = ({
/> />
</Field> </Field>
<Field <Field
label="Client Secret" label="Client secret"
htmlFor={`${formId}-oauth-secret`} htmlFor={`${formId}-oauth-secret`}
> >
<Input <Input
@@ -773,7 +773,7 @@ const ServerForm: FC<ServerFormProps> = ({
{form.values.authType === "api_key" && ( {form.values.authType === "api_key" && (
<div className="grid items-start gap-4 rounded-lg border border-solid border-border/70 bg-surface-secondary/30 p-4 sm:grid-cols-2"> <div className="grid items-start gap-4 rounded-lg border border-solid border-border/70 bg-surface-secondary/30 p-4 sm:grid-cols-2">
<Field <Field
label="Header Name" label="Header name"
htmlFor={`${formId}-apikey-header`} htmlFor={`${formId}-apikey-header`}
> >
<Input <Input
@@ -1048,7 +1048,7 @@ const ServerForm: FC<ServerFormProps> = ({
<div className="grid items-start gap-4 sm:grid-cols-2"> <div className="grid items-start gap-4 sm:grid-cols-2">
<Field <Field
label="Tool Allow List" label="Tool allow list"
htmlFor={`${formId}-allow-list`} htmlFor={`${formId}-allow-list`}
description="Comma-separated. Empty = all allowed." description="Comma-separated. Empty = all allowed."
> >
@@ -1061,7 +1061,7 @@ const ServerForm: FC<ServerFormProps> = ({
/> />
</Field> </Field>
<Field <Field
label="Tool Deny List" label="Tool deny list"
htmlFor={`${formId}-deny-list`} htmlFor={`${formId}-deny-list`}
description="Comma-separated names to block." description="Comma-separated names to block."
> >
@@ -272,7 +272,7 @@ export const MCPServerPicker: FC<MCPServerPickerProps> = ({
<button <button
type="button" type="button"
disabled={disabled} disabled={disabled}
aria-label="MCP Servers" aria-label="MCP servers"
className="group flex h-8 w-full cursor-pointer items-center gap-1.5 border-none bg-transparent px-1 text-xs text-content-secondary shadow-none transition-colors hover:text-content-primary disabled:cursor-not-allowed disabled:opacity-50" className="group flex h-8 w-full cursor-pointer items-center gap-1.5 border-none bg-transparent px-1 text-xs text-content-secondary shadow-none transition-colors hover:text-content-primary disabled:cursor-not-allowed disabled:opacity-50"
> >
<span>MCP</span> <span>MCP</span>
@@ -66,7 +66,7 @@ export const PersonalInstructionsSettings: FC<
return ( return (
<form className="flex flex-col gap-2" onSubmit={form.handleSubmit}> <form className="flex flex-col gap-2" onSubmit={form.handleSubmit}>
<h3 className="m-0 text-sm font-semibold text-content-primary"> <h3 className="m-0 text-sm font-semibold text-content-primary">
Personal Instructions Personal instructions
</h3> </h3>
<p className="m-0 text-xs text-content-secondary"> <p className="m-0 text-xs text-content-secondary">
Applied to all your conversations. Only visible to you. Applied to all your conversations. Only visible to you.
@@ -107,7 +107,7 @@ export const RetentionPeriodSettings: FC<RetentionPeriodSettingsProps> = ({
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="m-0 text-sm font-semibold text-content-primary"> <h3 className="m-0 text-sm font-semibold text-content-primary">
Conversation Retention Period Conversation retention period
</h3> </h3>
</div> </div>
<Switch <Switch
@@ -76,7 +76,7 @@ export const SystemInstructionsSettings: FC<
<form className="flex flex-col gap-2" onSubmit={form.handleSubmit}> <form className="flex flex-col gap-2" onSubmit={form.handleSubmit}>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="m-0 text-sm font-semibold text-content-primary"> <h3 className="m-0 text-sm font-semibold text-content-primary">
System Instructions System instructions
</h3> </h3>
</div> </div>
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
@@ -78,7 +78,7 @@ export const UsageIndicator: FC = () => {
sections.push({ sections.push({
id: "ai-usage", id: "ai-usage",
title: `${periodLabel} Usage`, title: `${periodLabel} usage`,
progressLabel: `${periodLabel} spend usage`, progressLabel: `${periodLabel} spend usage`,
percent: getPercent(currentSpend, spendLimit), percent: getPercent(currentSpend, spendLimit),
severity: getSeverity(currentSpend, spendLimit), severity: getSeverity(currentSpend, spendLimit),
@@ -58,7 +58,7 @@ const parseThresholdDraft = (value: string): number | null => {
const ContextCompactionHeader: FC = () => ( const ContextCompactionHeader: FC = () => (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<h3 className="m-0 text-sm font-semibold text-content-primary"> <h3 className="m-0 text-sm font-semibold text-content-primary">
Context Compaction Context compaction
</h3> </h3>
<p className="!mt-0.5 m-0 text-xs text-content-secondary"> <p className="!mt-0.5 m-0 text-xs text-content-secondary">
Control when conversation context is automatically summarized for each Control when conversation context is automatically summarized for each
@@ -78,7 +78,7 @@ export const VirtualDesktopSettings: FC<VirtualDesktopSettingsProps> = ({
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="m-0 text-sm font-semibold text-content-primary"> <h3 className="m-0 text-sm font-semibold text-content-primary">
Virtual Desktop Virtual desktop
</h3> </h3>
<Badge size="sm" variant="warning" className="cursor-default"> <Badge size="sm" variant="warning" className="cursor-default">
<TriangleAlertIcon className="size-3" /> <TriangleAlertIcon className="size-3" />
@@ -121,7 +121,7 @@ export const WorkspaceAutostopSettings: FC<WorkspaceAutostopSettingsProps> = ({
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className="m-0 text-sm font-semibold text-content-primary"> <h3 className="m-0 text-sm font-semibold text-content-primary">
Workspace Autostop Fallback Workspace autostop fallback
</h3> </h3>
</div> </div>
<Switch <Switch
@@ -140,7 +140,7 @@ export const WorkspaceAutostopSettings: FC<WorkspaceAutostopSettingsProps> = ({
<DurationField <DurationField
valueMs={form.values.workspace_ttl_ms} valueMs={form.values.workspace_ttl_ms}
onChange={handleTTLChange} onChange={handleTTLChange}
label="Autostop Fallback" label="Autostop fallback"
disabled={isSavingWorkspaceTTL || isWorkspaceTTLLoading} disabled={isSavingWorkspaceTTL || isWorkspaceTTLLoading}
error={Boolean(fieldError)} error={Boolean(fieldError)}
helperText={fieldError} helperText={fieldError}