## Problem
Generated reference docs (`docs/reference/cli/*`,
`docs/reference/api/*`) contained raw placeholder and JSON syntax that
came straight from Go CLI help strings and swagger annotations. HTML
renderers treat the angle-bracket tokens (`<team-slug>`, `<uuid>`,
`<KEY>`, etc.) as unknown tags and drop them, so readers see
broken/half-missing text today. The same strings also break MDX parsing.
## Fix
Wrap the placeholder/JSON syntax in backticks **at the source** (Go help
strings and swagger annotation comments), then `make gen`. Rendered docs
now show the placeholders as inline code instead of dropping them.
### Source changes
| File | Placeholder wrapped | Surfaces in |
|------|--------------------|-------------|
| `codersdk/deployment.go` | `` `<organization-name>/<team-slug>` `` |
`cli/server.md`, `coder --help`, settings UI |
| `codersdk/deployment.go` | `` `CODER_AI_GATEWAY_PROVIDER_<N>_*` ``, ``
`CODER_AI_GATEWAY_PROVIDER_<N>_<KEY>` `` | `api/schemas.md` |
| `cli/tokens.go` | `` `<type>:<uuid>` `` | `cli/tokens_create.md`,
`coder --help` |
| `coderd/aitasks.go` | `` `owner:<…>` ``, `` `organization:<…>` ``, ``
`status:<status>` `` | `api/tasks.md` |
| `coderd/exp_chats.go` | `` `pr_status:<…>` `` and sibling filter
tokens | `api/chats.md` |
| `coderd/provisionerdaemons.go`, `coderd/provisionerjobs.go` | ``
`{'tag1':'value1','tag2':'value2'}` `` | `api/organizations.md`,
`api/provisioning.md` |
Everything else in the diff (`coderd/apidoc/*`, `docs/reference/**`,
`*.golden`, `site/src/api/typesGenerated.ts`) is `make gen` output.
## Reviewer notes (the "considered pass" from the ticket)
- **Product-visible:** this changes `coder server --help` and `coder
tokens create --help` output, and the `server-config.yaml` reference
comment. Backticks in terminal help are literal but read fine as
placeholder markers.
- **Settings UI:** the `deployment.go` `Description` also renders in the
deployment settings page. If that field is not Markdown-rendered,
literal backticks will show there. Happy to drop the `deployment.go`
change if you'd rather keep the UI text clean and fix `server.md`
another way.
- **Out of scope here:** `docs/reference/cli/agent-firewall.md`
(`<host>`/`<glob>`) is generated from the external
`github.com/coder/boundary` module, not this repo. It needs an upstream
fix + module bump; not included in this PR.
<details>
<summary>Implementation notes / decision log</summary>
- Scope taken from DOCS-551: source-level backtick pass for generated
reference docs only. Hand-written Markdown fixes are tracked separately
(companion ticket).
- Swagger `@Param` descriptions are Go comments, so the existing `\|`
pipe-escaping in the chats `q` filter is preserved inside the new
backticks (still required for the Markdown table cell to render `|`).
- Verified after `make gen`: generated docs render placeholders as code
spans, table pipes intact; `gofmt` clean; changed Go packages build; no
emdash/endash introduced.
- Deliberately left the `AIProviderConfig` type-level doc comment
untouched because it does not surface in any generated doc (kept the
diff to doc-feeding comments).
</details>
Linear: DOCS-551
---
_Opened by Coder Agents on behalf of @nickvigilante._
---
## Evidence: placeholders dropped on the live docs site
Verified **2026-07-14** against the live site (`coder.com/docs`, i.e.
`main`, pre-merge) by loading each affected page in headless Chrome and
reading the post-hydration DOM (confirmed identical in the raw page
payload). Each simple `<token>` placeholder is parsed as an **empty
custom HTML element**, so the browser renders nothing for it and the
placeholder text disappears from the page.
### What readers see today (before this PR)
| Page (live) | Source Markdown | Rendered on the live site |
|-------------|-----------------|---------------------------|
| [`cli/server`](https://coder.com/docs/reference/cli/server) — OAuth2
GitHub Allowed Teams | `Structured as: <organization-name>/<team-slug>.`
| `Structured as: /.` |
|
[`cli/tokens_create`](https://coder.com/docs/reference/cli/tokens_create)
— `--allow` | `Repeatable allow-list entry (<type>:<uuid>, e.g.
workspace:1234-...).` | `Repeatable allow-list entry (:, e.g.
workspace:1234-...).` |
| [`api/tasks`](https://coder.com/docs/reference/api/tasks) — `q` | `...
status:<status>` | `... status:` (nothing after the colon) |
| [`api/schemas`](https://coder.com/docs/reference/api/schemas) —
AIBridgeConfig (`anthropic`/`bedrock`/`openai`) |
`CODER_AI_GATEWAY_PROVIDER_<N>_*` | `CODER_AI_GATEWAY_PROVIDER__*` |
| [`api/schemas`](https://coder.com/docs/reference/api/schemas) —
AIBridgeConfig (`providers`) | `CODER_AI_GATEWAY_PROVIDER_<N>_<KEY>` |
`CODER_AI_GATEWAY_PROVIDER__` |
[`api/chats`](https://coder.com/docs/reference/api/chats) (`q`) drops
five tokens the same way — `title:<substring>`, `diff_url:<url>`,
`pr:<number>`, `pr_title:<text>`, and the trailing `title:<value>`. The
live parameter description reads (note the dangling `title:`,
`diff_url:`, `pr:`, `pr_title:`):
```text
Search query. Supports title: (case-insensitive, quote multi-word values), archived:bool, has_unread:bool, pr_status:<draft|open|merged|closed> as repeated or comma-separated values, source:<created_by_me|shared_with_me>, diff_url: (quote values containing colons), pr: (exact PR number match), repo:<owner/repo> (case-insensitive substring match against git remote origin or URL), pr_title: (case-insensitive PR title substring). Bare terms are not supported; use title: for title filtering.
```
<details>
<summary>Raw rendered DOM from the live site (headless Chrome,
post-hydration)</summary>
```html
<!-- reference/cli/server -->
Structured as: <organization-name>/<team-slug>.</team-slug></organization-name>
<!-- reference/cli/tokens_create -->
Repeatable allow-list entry (<type>:<uuid>, e.g. workspace:1234-...).</uuid></type>
<!-- reference/api/tasks : only status:<status> drops; the /-containing tokens are escaped and survive -->
Search query for filtering tasks. Supports: owner:<username/uuid/me>, organization:<org-name/uuid>, status:<status></status>
<!-- reference/api/schemas : anthropic / bedrock / openai rows -->
Deprecated: Use Providers with indexed CODER_AI_GATEWAY_PROVIDER_<n>_* env vars instead.</n>
<!-- reference/api/schemas : providers row -->
Providers holds provider instances populated from CODER_AI_GATEWAY_PROVIDER_<n>_<key> env vars and/or the deprecated LegacyOpenAI/LegacyAnthropic/LegacyBedrock fields above.</key></n>
```
The parser auto-inserts closing tags
(`</team-slug></organization-name>`) and lowercases the tag name (`<N>`
becomes `<n>`), leaving `__` where `<N>_` used to be. Every wrapped
placeholder renders correctly as inline code on the [docs preview for
this
branch](https://coder.com/docs/@vigilante%2Fdocs-551-backtick-placeholder-syntax-in-generated-reference-docs-cli/reference/cli/server).
</details>
### Accuracy note — cases that do *not* drop on live
These render fine today, so they are **not** evidence of dropping (the
PR still wraps them for consistency / MDX-safety):
-
[`api/organizations`](https://coder.com/docs/reference/api/organizations)
and
[`api/provisioning`](https://coder.com/docs/reference/api/provisioning):
`{'tag1':'value1','tag2':'value2'}` renders verbatim — curly braces are
not an HTML tag.
- Tokens containing `/` or `|` are escaped by the renderer and stay
visible (as literal `<...>`): `<username/uuid/me>`, `<org-name/uuid>`,
`<owner/repo>`, `<draft|open|merged|closed>`,
`<created_by_me|shared_with_me>`. Backticks still improve their
readability, but they were never dropped.
Adds `--aigateway-proxy-target` option to
`deploymentGroupAIGatewayProxy` that defines URL to which intercepted
requests should be forwarded to.
Forward URL used to be hardcoded to `coderAPI.AccessURL` pointing to
embedded Gateway. With addition of standalone AI Gateway this needs to
be configurable.
Renamed `aibridgeproxyd.Server.coderAccessURL` and `coderAccessPort` ->
`gatewayURL` and `gatewayPort` + option to better reflect reality.
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
## Problem
#27191 fixed the mobile menu missing the **AI** / **AI sessions** items,
but did so by duplicating the item list. The desktop
`DeploymentDropdown` and mobile `MobileMenu` each hardcode the same
Admin settings links and permission gates. That duplication is exactly
why the two drifted out of sync in the first place, and it will happen
again the next time an item is added.
## Fix
Extract a single source of truth in `adminSettings.ts`:
- `AdminSettingsPermissions` type shared by both surfaces.
- `getAdminSettingsItems(permissions)` builds the ordered item list
using conditional spreads, e.g.
```ts
...(canViewAISettings ? [{ label: "AI", to: "/ai/settings" }] : []),
```
- `canViewAdminSettings(permissions)` for the desktop visibility gate.
`DeploymentDropdown` and `MobileMenu` now just `.map()` over the shared
list, so adding or changing an item is a one-line edit in one place.
No backend, permission, routing, or user-visible behavior changes. Item
labels, links, and order match the current desktop dropdown (mobile now
inherits the same `linkToAuditing` constant instead of a hardcoded
`/audit`, same value).
<details>
<summary>Rationale / approach</summary>
Following Larry Wall's virtues: laziness (one list to maintain, not
two), impatience (kill the class of bug where the two menus silently
diverge), and hubris (leave a shared module nobody has to apologize
for).
Kept `canViewOrganizations` in the permission type and visibility gate
even though Organizations always renders, preserving prior behavior
rather than changing it as part of a refactor.
Validated with `biome check` and `tsc --noEmit`; existing
`MobileMenu.stories.tsx` args already cover the Admin / Auditor /
OrgAdmin / Member permission matrices.
</details>
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
## What
Removes the last `@emotion/react` usage from the DERP health page
(`site/src/pages/HealthPage/DERPPage.tsx`).
- Drops `useTheme` from `@emotion/react`; the page no longer reads the
Emotion theme.
- Replaces the region pin's `style={{ color: healthyColor(theme,
severity) }}` with a Tailwind class derived from severity, matching the
existing pattern in sibling components (`HealthIcon`, `HealthyDot`,
`StatusIcon` in `Content.tsx`).
- Deletes `healthyColor.ts`, which was only consumed by this page and
itself depended on `@emotion/react`.
## Why
The page was already almost entirely Tailwind-based; the theme lookup
was the only Emotion holdout. Aligning it with the surrounding
components keeps the severity colours consistent and removes dead code.
<details>
<summary>Notes</summary>
Severity → class mapping mirrors `Content.tsx`:
| Severity | Class |
| --- | --- |
| `ok` | `text-content-success` |
| `warning` | `text-content-warning` |
| `error` | `text-content-destructive` |
Note: the old helper used `theme.roles.*.fill.solid` tokens, whereas the
surrounding components use the `content-*` tokens — these can differ
slightly in shade. I opted for consistency with the neighbouring health
components. Happy to swap to a dedicated `fill`-equivalent token if
you'd prefer an exact colour match.
</details>
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
Removes the remaining MUI/Emotion dependency from `TokensPageView.tsx`.
## Changes
- Removed `useTheme` from `@emotion/react` and the `const theme =
useTheme()` call.
- Replaced all `style={{ color: theme.palette.text.secondary }}` inline
styles with the `text-content-secondary` Tailwind token, matching
sibling components (`Table.tsx`, other UserSettings pages).
- Preserved `data-pixel="ignore"` and all other attributes/behavior.
## Validation
- `tsc --noEmit`: no errors for the file.
- `biome check`: clean.
Browser back/forward controls did not work in the template builder
wizard. Using browser navigation sent the user out of the wizard
entirely instead of moving between steps.
The wizard's current step is now driven by a `?step=` search param. Step
transitions push history entries so browser back/forward walks the
steps. Steps that are invalid, skipped, or unreachable given the wizard
state are clamped and the URL is rewritten with a replace, keeping URL
and state in sync in both directions.
`?base=` is accepted as the only entry search param and deep-links to a
preselected base template (superseding #26808). The page container
resolves it against the available bases, holds the wizard behind a
loader until it is consumed and removed from the URL, then mounts the
wizard starting on the first step after base selection.
Closes https://linear.app/codercom/issue/DEVEX-594
> Generated with [Coder](https://coder.com) by @jeremyruppel
Implements
https://linear.app/codercom/issue/AIGOV-213/add-bedrock-provider
# AWS Bedrock mantle support in AI Gateway
## Summary
Add support for the AWS Bedrock **mantle** endpoint
(`bedrock-mantle.{region}.api.aws/anthropic/v1/messages`) to AI Gateway.
Mantle serves Claude through the native Anthropic Messages API. We model
it as a `protocol` field on the existing Bedrock provider settings
(`invoke-model` default, or `mantle`) rather than as a new provider
type, and we treat mantle as a pure passthrough: SigV4-sign and forward,
no body translation.
## Background
Claude on AWS Bedrock is reachable through two endpoints, each speaking
exactly one wire protocol:
1. **InvokeModel** (existing): `bedrock-runtime.{region}.amazonaws.com`.
Model id in the URL path, request translated into Bedrock's InvokeModel
format, responses returned as a binary AWS eventstream. This is what AI
Gateway already supported for Bedrock.
2. **Mantle** (this doc):
`bedrock-mantle.{region}.api.aws/anthropic/v1/messages`. Native
Anthropic Messages API: model in the body, plain SSE streaming.
## Why a `protocol` field, not a new provider type
The alternative is to model mantle as its own `ai_provider_type`
(`bedrock-mantle`) alongside `bedrock`. I chose the `protocol` field
instead for two reasons:
1. Mantle reads more like a protocol of Bedrock than a separate
provider. It is the same AWS account, credentials, region, and IAM,
reached over a different wire protocol and host. One Bedrock provider
with two protocols (`invoke-model` default and `mantle`) models that
more organically than two provider types.
2. It avoids a database migration. The `protocol` field lives in the
settings JSON blob (empty resolves to `invoke-model`, so existing
providers are unaffected), whereas a new type means an enum value and
the `ALTER TYPE ... ADD VALUE` migration that goes with it.
## Why passthrough, not translation
The client already emits Bedrock-legal requests in mantle mode:
```sh
export CLAUDE_CODE_USE_MANTLE=1
export CLAUDE_CODE_SKIP_MANTLE_AUTH=1
export ANTHROPIC_BEDROCK_MANTLE_BASE_URL=https://<coder>/api/v2/aibridge/<provider-name>
```
So the gateway just forwards the body and SigV4-signs it (service
`bedrock-mantle`), and skips all the InvokeModel body-translation (model
remap, thinking conversion, beta-flag allowlist, field stripping). This
keeps the mantle path thin and avoids a second copy of translation logic
to maintain.
## Consequences
- Protocol-dependent fields: `model` / `small_fast_model` are used by
InvokeModel but ignored by mantle (the client sends the model), and
`base_url` is required for mantle but optional for InvokeModel.
Validation is protocol-aware.
- No central model control on mantle: because it is a passthrough, the
operator cannot pin the model.
- `region` and the `base_url` host must name the same region (the SigV4
scope must match the endpoint); a mismatch surfaces as `Credential
should be scoped to a valid region`.
## Draft UI
<img width="1100" height="579" alt="image"
src="https://github.com/user-attachments/assets/37bab46d-8958-4a96-9f47-1fef3493e1b6"
/>
## Follow-up PRs:
- https://github.com/coder/coder/pull/27156
## Problem
The desktop `DeploymentDropdown` surfaces **AI** and **AI sessions**
items under Admin settings (behind `canViewAISettings` /
`canViewAIBridge`), but the mobile `MobileMenu` was never updated to
match — its `AdminSettingsSub` only knows about Deployment,
Organizations, Audit logs, Connection logs, and Healthcheck.
This is a leftover from #25582 ("promote AI settings to a top-level
section"), which threaded `canViewAISettings` through `Navbar` /
`NavbarView` / `DeploymentDropdown` but did not touch `MobileMenu.tsx`.
`canViewAIBridge` has the same oversight.
## Fix
- Extend `MobileMenuPermissions` with `canViewAIBridge` and
`canViewAISettings`.
- Render **AI** (→ `/ai/settings`) and **AI Sessions** (→
`/ai-gateway/sessions`) in the mobile Admin settings collapsible, in the
same order as the desktop dropdown.
- Thread the two flags through `NavbarView` into `MobileMenu`.
- Cover the new args in `MobileMenu.stories.tsx` (Admin story now shows
the AI items; Auditor / OrgAdmin / Member keep them hidden).
No backend, permission, or routing changes.
> 🤖 This PR was drafted by Coder Agents on behalf of @tracyjohnsonux and
needs a human review.
fixes DEVEX-463
Previously, a dynamic parameter of `form_type = "multi-select"` with
default options selected would only pass in these 3 properties for each
option fed into `MultiSelectCombobox`'s `defaultOptions`:
- `value`
- `label`
- `disable`
`defaultOptions` are used as the initial value of
`MultiSelectCombobox`'s `selected` array of `Option`s. If the `icon`
property is missing from a selected option, then no icon is shown:
https://github.com/coder/coder/blob/e59a67d63fd522d1e95b42e57d4fbfe7d2bc5fef/site/src/components/MultiSelectCombobox/MultiSelectCombobox.tsx#L501-L502
Now `icon` and `description` are included among those properties passed
to default selected options, when a default option's value can be found
among the parameter's available options in the Terraform template. This
fixes the bug where icons wouldn't display for default selected options.
If no corresponding option is found, then we fall back to the old
partial option object (`{ value, label, disable }`).
<img width="1840" height="1191" alt="Screenshot 2026-07-09 at 10 08
54 PM"
src="https://github.com/user-attachments/assets/91be4aaf-9016-46d6-871b-344c8f2504cb"
/>
The chatd state machine only recognizes `waiting`, `running`, `error`,
`requires_action`, and `interrupting`. Remove the unused `pending`,
`paused`, and `completed` values from the database enum, backend, SDK,
frontend, generated queries, and API docs.
Migration `000543_chat_status_remove_unused` remaps existing `pending`
rows to `running`, remaps `paused` and `completed` rows to `waiting`,
drops the obsolete `idx_chats_pending` index, and recreates
`chats_expanded` around the enum swap. It also removes the dead
`AcquireChats` query and all remaining query literals for the deleted
statuses.
**NOTE**: The enum swap can break chat queries from older replicas
during a mixed-version rollout because they still reference
`'pending'::chat_status`. Chats are experimental, so this PR accepts
that limited rollout window instead of adding a two-release expand and
contract sequence.
> This PR was authored by Mux (AI agent) on Mike's behalf.
Finishing touches for the AI cost control group members table.
- Spend stays primary-colored until near the budget limit, via a new
AIBudgetAmount component
- "AI budget period" label shows the current spend window in local
time, next to the members tab
- Budget tooltip notes the reset date and the group's default limit
- "Budget type" renamed to "Budget group", with a badge for the
governing group or override
- Distinguishes $0 budget ("None") from no budget ("Unlimited")
- Unattributed spend from another group shows a note instead of a dash
- "Manage AI budget" disabled only when another named group governs
- Replaces UserAISpend with generated UserAISpendStatus, fixing
limit_source
Closes#26401
Fixes flake reported in
[DEVEX-538](https://linear.app/codercom/issue/DEVEX-538/flake-create-user-with-password).
## Problem
The `login()` e2e helper had a race condition causing intermittent
navigation failures:
```
page.goto: Navigation to "/deployment/users" is interrupted by
another navigation to "/"
```
After clicking Sign In, `LoginPage.tsx` does a hard navigation via
`location.href = sanitizeRedirect(redirectTo)`. With no `?redirect=`
param, `retrieveRedirect` defaults to `"/"`, so login navigates to `/`.
The browser loads `/`, fires the `load` event, then React boots and the
router does a client-side redirect from `/` to `/workspaces` (via
`<Navigate to="/workspaces" replace />`).
The old helper waited with
`expectUrl(page).toHavePathName("/workspaces")`, which polls
`page.url()` and resolves the moment the pathname matches. It has no
awareness of page load state. So it resolved after the client-side
redirect changed the URL, but before the `/workspaces` page components
had mounted. When a test immediately called `page.goto()` afterward,
pending React rendering could trigger a competing navigation.
## Fix
Replace the URL polling with two Playwright-idiomatic waits:
1. `page.waitForURL(/\/workspaces/)` hooks into the browser's navigation
lifecycle: it waits for the URL to match AND for the page to reach a
load state (`"load"` by default), unlike `expectUrl` which is purely a
string poll.
2. `await expect(page).toHaveTitle(/Workspaces/)` waits for the page
title, which is set by the `WorkspacesPage` component. This proves React
booted, auth resolved, and the page fully rendered, closing the window
where pending React work could interfere with the next navigation.
Also adds `{ waitUntil: "domcontentloaded" }` to `page.goto("/login")`
for consistency with every other navigation helper in the file.
> 🤖 Generated by Coder Agents on behalf of @jeremyruppel
The deployment-wide computer use provider was passed around as a bare `string` on the `codersdk` wire structs, in `chattool`, and in the generated TypeScript, and its valid values (`anthropic`, `openai`) were never exposed as a `codersdk` enum. That's out of step with our other chat settings (`ChatDebugRunKind`, `ChatUsageLimitPeriod`), which already define enums with `Valid()` and an `All<Name>s` slice, and it left the allowed values duplicated as literals with no typed contract for clients.
This adds `codersdk.ChatComputerUseProvider` as the single source of truth and routes the API boundary, `chattool`, `chatd`, and the generated TypeScript through it. The DB layer and chattool's internal model-provider routing stay `string` on purpose, since they handle untrusted or fantasy-model values that just happen to share the names.
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
The "Session completed" marker at the bottom of an AI Gateway session
timeline was rendered unconditionally, so on long sessions it appeared
below still-loading threads while the user scrolled. That is misleading:
users read it as the end of the session even when more threads are about
to stream in.
Only render the session end marker (rows 7 and 8 of the grid: the
connecting vertical line, the success dot, and the "Session completed"
text) once every thread has loaded, that is, once both `hasNextPage` and
`isFetchingNextPage` are false. The dashed timeline box still closes
cleanly at the bottom, and the infinite-scroll spinner keeps rendering
inside row 5 while more pages fetch.
| Old | New |
| --- | --- |
| <img width="1099" height="338" alt="preview-old-behaviour"
src="https://github.com/user-attachments/assets/f86c9ce1-f4ca-4088-a1ed-9cdcf8fb940c"
/> | <img width="1099" height="323" alt="preview-new-heaviour"
src="https://github.com/user-attachments/assets/b4a1e703-10d2-4378-9f25-48cac46249a1"
/> |
## Verification
Rendered each SessionTimeline story via a headless Chromium and asserted
whether "Session completed" is present:
| Story | `hasNextPage` | `isFetchingNextPage` | "Session completed" |
| --- | --- | --- | --- |
| OneThread | false | false | visible |
| MultipleThreads | false | false | visible |
| FetchingNextPage | true | true | hidden |
| HasMoreThreadsToLoad (new) | true | false | hidden |
All checks pass locally:
- `pnpm format` (no changes)
- `pnpm lint:check`
- `pnpm lint:types`
- `make pre-commit` via githooks
<details>
<summary>Implementation notes</summary>
-
`site/src/pages/AIBridgePage/SessionThreadsPage/SessionTimeline/SessionTimeline.tsx`:
wrap the row 7 spacer and row 8 status dot/text in `!hasNextPage &&
!isFetchingNextPage`.
- `SessionTimeline.stories.tsx`: add `HasMoreThreadsToLoad` to cover the
between-fetches state.
- No prop signature or public API change; `SessionTimelineSkeleton.tsx`
is untouched because the skeleton is only shown before any threads have
loaded.
</details>
closes DEVEX-588
Prototyped in #27077, broken off into a separate PR to make this work
easier to track
## changes
- Reveals the previously hidden trash can icon within
`ModuleConfiguration` (main content area)
- Removes the "x" icons from `ModuleSelection` (sidebar)
## context
@tracyjohnsonux and I decided [in
Slack](https://codercom.slack.com/archives/C0AUKB54P0E/p1783456607073329?thread_ts=1783450189.570979&cid=C0AUKB54P0E)
that it would be a better UX to move the deletion action from the "x"
icons in the sidebar to the trash can icons in the main content area.
This change has the benefits of
1. making it harder to delete modules accidentally
2. removing the responsibility of deletion from the items in
`ModuleSelection`
- interacting with these items will serve only to navigate to
configuring that module (DEVEX-587, to be done in a separate PR)
<img width="1840" height="1191" alt="image"
src="https://github.com/user-attachments/assets/4571ea2f-75cf-4cd7-b626-1826eea83bdf"
/>
Previously, bulk start required every selected workspace to be stopped,
and bulk stop required every selected workspace to be running. Mixed
selections disabled both buttons entirely.
- Change the disabled checks on bulk start/stop from `every()` to
`some()` so the buttons are enabled when at least one workspace is
eligible.
- Filter workspaces by status in the mutation functions so only eligible
workspaces are sent to the API, matching the pattern used by other batch
mutations (update, favorite, unfavorite).
- Update docs to reflect the new behavior.
> [!NOTE]
> Generated by Coder Agents. [View session](https://coder.com/).
<details>
<summary>Implementation plan</summary>
## Problem
When an admin selects multiple workspaces and opens the "Bulk actions"
dropdown, the **Start** menu item is disabled unless *every* selected
workspace has `latest_build.status === "stopped"`. If even one workspace
is already running (or in any other non-stopped state), the Start button
is grayed out and unusable. Same issue applies to **Stop**.
## Changes
### 1. Relax disabled condition (`WorkspacesPageView.tsx`)
Changed `every()` to `some()` for both Start and Stop dropdown items.
The buttons are now enabled when at least one selected workspace is in
the target state.
### 2. Filter in mutations (`batchActions.ts`)
Added `.filter()` before `.map()` in both `startAllMutation` and
`stopAllMutation` so only eligible workspaces hit the API. This matches
the existing pattern in `updateAllMutation`, `favoriteAllMutation`, and
`unfavoriteAllMutation`.
### 3. Update documentation (`docs/user-guides/workspace-management.md`)
Replaced "can only be applied to a set of workspaces which are all in
the same state" with "apply to eligible workspaces in the selection,
skipping workspaces that are already in the target state."
## Testing
Four new Storybook stories:
| Story | What it tests |
|-------|---------------|
| `StartIgnoresAlreadyRunningWorkspaces` | Mixed selection; only stopped
workspaces get `startWorkspace` calls |
| `StopIgnoresAlreadyStoppedWorkspaces` | Mixed selection; only running
workspaces get `stopWorkspace` calls |
| `StartDisabledWhenNoWorkspacesAreStartable` | All running; Start
button is disabled |
| `StopDisabledWhenNoWorkspacesAreStoppable` | All stopped; Stop button
is disabled |
</details>
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
The UI guard added in #22112 disabled the `Activity bump` field and
cleared its saved value whenever the template's `Default autostop` was
0. It did not check the "Allow users to customize autostop duration for
workspaces" (`allow_user_autostop`) setting, so templates that relied on
user-defined autostop timers had their `activity_bump_ms` silently
cleared when saving in the Coder UI.
Enable the field, preserve the value on submit, and update the helper
text when either `default_ttl_ms > 0` or `allow_user_autostop` is true.
Closes
[DEVEX-438](https://linear.app/codercom/issue/DEVEX-438/allow-user-autostop-default-autostop-disabled-causes-activity-bump-to).
> **Note:** This needs to be backported to 2.34 (ESR).
<details>
<summary>Implementation notes</summary>
### Problem
[#22112](https://github.com/coder/coder/pull/22112) introduced a UI
guard that:
1. Disables the `Activity bump (hours)` field when `default_ttl_ms ===
0`.
2. Sends `activity_bump_ms: undefined` on submit under the same
condition, which the backend treats as "do not update", but combined
with the disabled state users cannot re-enter a value once cleared and
the previously stored value effectively becomes orphaned.
The guard ignored `allow_user_autostop`. When that setting is enabled,
workspaces still have a scheduled stop (whatever the user configures on
their workspace), so `activity_bump_ms` is still meaningful.
### Fix
Broaden the guard to consider both signals. The field is only disabled
and the value only discarded when **both** `default_ttl_ms === 0`
**and** `allow_user_autostop === false`.
Changes:
- `TemplateScheduleForm.tsx`
- `disabled` prop now checks `!default_ttl_ms && !allow_user_autostop`.
- Submit path preserves `activity_bump_ms` when either signal is truthy.
- Passes `allowUserAutostop` through to the helper text.
- `TTLHelperText.tsx`
- `ActivityBumpHelperText` accepts `allowUserAutostop` and only shows
the "no scheduled stop" hint when neither signal is set. Updated copy
mentions both signals.
- Tests and stories
- Existing tests explicitly uncheck `allow_user_autostop` before
asserting the guard fires (since `MockTemplate.allow_user_autostop`
defaults to `true`).
- Added coverage: guard stays off when only `allow_user_autostop` is
enabled; toggling `allow_user_autostop` re-enables the field without
touching `default_ttl_ms`.
- Added a story that verifies `activity_bump_ms` is preserved on submit
when `allow_user_autostop` is enabled and `default_ttl_ms` is 0.
</details>
Categorises the terminal error of a failed interception and persists it
on the interception record, then surfaces it on the AI Gateway API.
- Categorise into an enum (`bad_request`, `unauthorized`,
`rate_limited`, `overloaded`, `server_error`, `unknown`), unwrapping
the ResponseError envelope, the upstream Anthropic/OpenAI SDK errors,
and key-pool exhaustion so blocking and streaming paths agree.
- Thread the type and raw message through the recorder dRPC into the
`aibridge_interceptions` row (optional proto fields; NULL on success).
- Expose the error on the AI Gateway thread API from the root
interception.
*This PR was produced by opencode (agent) using the `anthropic/claude-opus-4-8` model, under human direction and review.*
Child chats (sub-agent chats) no longer offer archive-state actions in
their menus. Archive state is root-only on the backend and cascades to
children (`coderd/exp_chats.go` rejects `archived` changes when
`parent_chat_id` is set), so a child's "Archive agent", "Archive &
delete workspace", and "Unarchive agent" items always failed with a 400.
All chat action menus (chat header kebab, sidebar row dropdown, sidebar
right-click context menu) render the shared `ChatActionsMenuItems`,
which already hides Pin/Unpin for child chats; this extends the same
gating to the archive and unarchive items.
Since an archived child chat then has no menu actions at all, the menu
triggers are hidden for archived child chats (`chatHasMenuActions`): the
header kebab and the sidebar row's dropdown trigger are not rendered,
and the row's right-click context menu is disabled. Archived root chats
keep their "Unarchive agent" action.
Stories: renamed the ChatTopBar child-chat story to
`ChildChatHidesPinAndArchiveActions` and extended it to assert both
archive items are hidden, plus new stories for the archived-child cases
(`ArchivedChildChatHasNoActionsMenu`,
`ArchivedChildChatRowHasNoActionsMenu`) and a sidebar child-menu story
(`ChildChatMenuHidesArchiveActions`).
Closes CODAGT-631.
> This PR was created by Mux, an AI agent working on behalf of Mike.
## Description
Adds the `GET /api/v2/users/{user}/ai/spend` endpoint returning the
user's current AI spend, effective budget, and period bounds.
## Changes
- Add `userAISpendStatus` handler under the same feature/experiment gate
as `/api/v2/users/{user}/ai/budget`.
- Add `codersdk.UserAIBudgetSummary` (embedded into `UserAISpendStatus`)
and a `UserAISpendStatus` client method.
- Move `LimitSource` from `coderd/aibridge/budget` to `codersdk` so the
type is shared across endpoints.
Closes https://linear.app/codercom/issue/AIGOV-472
> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by
@ssncferreira
Migrates the WorkspacesPage tests from vitest to Storybook play-function stories.
The old `WorkspacesPage.test.tsx` rendered the page through `renderWithAuth` and MSW, which is slow and contributes to `test-js` timeout flakes. The new stories seed the react-query cache directly and assert the same behavior in `play` functions, so they run as Storybook interaction tests instead of in the vitest `unit` project.
Coverage is preserved across all flows from the old test: rendering the empty and filled pages, deleting only the selected workspaces, the three batch-update cases (skipping up-to-date workspaces, updating a running workspace after acknowledging the restart risk, and ignoring dormant workspaces), stopping only the selected running workspaces, starting only the selected stopped workspaces, filtering workspace apps by health and visibility, and hiding the start button for an outdated stopped always-update workspace. The entire `WorkspacesPage.test.tsx` file is removed since it contained no pure-logic tests to keep.
Closes CODAGT-686
Migrates the TemplateSettingsPage tests from vitest to Storybook play-function stories.
The old `TemplateSettingsPage.test.tsx` rendered the full settings layout through `renderWithTemplateSettingsLayout` and MSW, which is slow and contributes to `test-js` timeout flakes. The new stories seed the react-query cache directly and assert the same behavior in `play` functions, so they run as Storybook interaction tests instead of in the vitest `unit` project.
Coverage is preserved across four flows: a successful metadata update, the validation error surfaced in the form when the name is already taken, deprecating a template when the access_control entitlement is present, and leaving the deprecation message empty when it is not. The pure-logic description validation tests stay in `TemplateSettingsPage.test.tsx`.
Relates to CODAGT-686
Migrates the WorkspaceSchedulePage tests from vitest to Storybook play-function stories.
The old `WorkspaceSchedulePage.test.tsx` rendered the full settings layout through `renderWithWorkspaceSettingsLayout` and MSW, which is slow and contributes to `test-js` timeout flakes. The new stories seed the react-query cache directly and assert the same behavior in `play` functions, so they run as Storybook interaction tests instead of in the vitest `unit` project.
Coverage is preserved across four flows: enabling autostop seeds the template's default TTL, changing autostop on a running workspace shows the restart dialog after a successful save, a stopped workspace skips that dialog, and changing only autostart skips it as well. The pure-logic schedule and TTL conversion tests stay in `WorkspaceSchedulePage.test.tsx`.
Relates to CODAGT-686
This models restart as durable orchestration of existing stop and
start workspace builds instead of adding a new restart transition.
Keeping restart as two existing transitions preserves the current
build/provisioner model.
The child start build is created only after the parent stop build
succeeds, rather than being inserted immediately in a pending
state. That keeps `workspace_builds` aligned with actual
provisioner-ready work and avoids introducing a second
pending-build lifecycle that the provisioner and build acquisition
paths would need to understand.
Refs: https://linear.app/codercom/issue/PLAT-143
Selecting Today in the AI spend usage date range picker sent an end date
rounded to the next hour, which made refreshed usage pages show stale
partial-day data.
This lets the shared date range picker keep its existing next-hour
default for template insights while the spend usage and drill-in pickers
request an end-of-day boundary for Today.
Closes
https://linear.app/codercom/issue/CODAGT-751/date-range-picker-uses-end-of-hour-for-today
Generated by Coder Agents.
Fixes a bug where if `base.variables` is not defined, clicking a
`TemplateCard` fails (no base template gets selected) and this error
gets logged:
```
Uncaught TypeError: Cannot read properties of undefined (reading 'length')
```
<img width="1840" height="1191" alt="image"
src="https://github.com/user-attachments/assets/b51a1f05-ab58-4b17-a126-350604f9832b"
/>
Variables with defaults showed the default as placeholder text in an
empty field. If the user did not type anything, an empty value was sent
instead of the default. Now the default pre-populates the field value so
it is visible and editable.
Fixes both `BaseTemplateParametersStep` and `ModuleSettingsStep` by
falling back to `defaultPlaceholder(v.default)` instead of `""` when no
user value exists.
Fixes DEVEX-584
> [!NOTE]
> This PR was authored by Coder Agents on behalf of @jeremyruppel.
## Problem
The Generate button in the chat Rename dialog (POST
`/api/experimental/chats/{chat}/title/propose`) could fail in ways
unrelated to actual concurrent title generation:
- The manual title lock returned 409 for any `pending` chat and any
`running` chat without a worker. Legacy `pending` rows are never
acquired by workers, so those chats 409'd forever. Running chats are
unowned in the normal window between message submission and worker
acquisition (indefinitely when runners are down), producing spurious
409s.
- A missing default chat model config surfaced as a generic 500, and the
dialog hid the actionable cause carried in the error detail.
## Fix
Backend (`coderd/x/chatd`, `coderd`, `coderd/database`):
- Remove the manual title lock entirely. Races between title writers are
already resolved by `recordManualTitleUsage`, which re-reads the chat
under `GetChatByIDForUpdate` and only persists the generated title when
it is unchanged since the request snapshot, so concurrent regenerates
and renames settle by last write wins. The lock only suppressed
duplicate model calls (the dialog already disables the button in flight,
and usage limits bound spend), and its synthetic `worker_id` marker was
the source of the spurious 409s. The 409 responses, the marker and
staleness handling, and the now-unused
`UpdateChatStatusPreserveUpdatedAt` query are gone.
- New `ErrNoDefaultChatModelConfig` sentinel mapped to 400 "No default
chat model config is configured." in both title endpoints, matching the
POST `/chats` precedent.
Frontend (`site`):
- The Rename dialog error alert now renders the API error detail under
the message, reading `error.response.data.detail` directly so
detail-less API errors do not show the generic developer-console hint.
- Removed the dead regenerate-title UI plumbing (`onRegenerateTitle`
outlet wiring and the `regeneratingTitleChatIds` spinner pipeline). The
Rename dialog propose flow is the only live title-generation UX; the
endpoint, codersdk methods, and the `api.ts`/`queries/chats.ts` layer
are kept for API consumers.
## Tests
- chatd internal: a strict-mock test pinning the compare-and-swap guard
(a concurrently changed title must not be clobbered by a generated one),
plus the existing persist-and-broadcast coverage without lock
transactions.
- HTTP: `PendingWithoutWorker` expects 200 for both endpoints,
`NoDefaultModelConfig` (400) subtests, a stopped-workspace propose
regression, and an `Unauthenticated` propose subtest.
- Storybook: stories asserting the API error detail renders in the
dialog alert, and that detail-less API errors and plain errors do not
leak the developer-console hint.
> Authored by Mux on Mike's behalf.
---------
Co-authored-by: Mathias Fredriksson <mafredri@gmail.com>
Followup to #26907, which re-enabled sticky positioning within
`#main-content` throughout the site. From
https://github.com/coder/coder/pull/26907#issuecomment-4860536495:
>Updating [`HorizontalSection` in OrganizationSettingsPageView.tsx]'s
sticky styles will be a little more involved, so I'll save that for a
separate PR.
## before
Note that I inflated this section's height to 1000px in DevTools to be
able to demo sticky positioning not working. This page isn't tall enough
at the moment for the lack of sticky positioning to be much of a
problem. But this will future-proof it (since `FormSection`, whose
sticky styles were fixed in #26907, was built for this purpose) and
allow us to remove some redundant components/files. No other files
imported `HorizontalContainer` or `HorizontalSection`, so
OrganizationSettingsPage/Horizontal.tsx can be safely deleted.
Also note that the **Info** section is sticky as expected, since it was
already using `FormSection`. The **Workspace Sharing** and **Delete
Organization** sections are the ones whose stickiness is fixed by this
PR
https://github.com/user-attachments/assets/9ffed486-87f7-4762-a97a-4268d0328829
## after
https://github.com/user-attachments/assets/581cb33e-50b5-466e-9be9-2518dc22bfa6