In summary, we use the init message if we have no autofill params. If we
do, then we ignore the init message, send another message with the
autofilled values, and then when we get *that* response, finally we
render the form since we know we have good state. This eliminates the
possibility of temporarily rendering with stale state.
This is the same fix that was applied to the edit page, but on the
create page this time. The only difference is that the create page does
not need to wait on a build parameters query, instead it has to wait on
the first message from the socket (to get defaults).
This also makes one change where we would send the defaults in the init
message. The server already knows the defaults so there is no need to
send them. The advantage here is that we no longer need to wait for the
first message, and it also fixes an issue where fields with blank values
were getting validation errors because they were not filled out, before
the user had a chance to actually fill them out.
Closes
[CODAGT-804](https://linear.app/codercom/issue/CODAGT-804/add-ui-option-to-revoke-oauth-mcp-credentials).
Users could authenticate with an OAuth2 MCP server from the chat input,
but there was no UI to disconnect those per-user credentials. The
backend endpoint (`DELETE
/api/experimental/mcp/servers/{id}/oauth2/disconnect`) already existed.
## Changes
- Connected OAuth2 MCP rows in the chat input plus menu now show a
disconnect icon button next to the enable switch. It opens a
confirmation dialog; confirming calls the disconnect endpoint, shows a
toast, and refetches MCP configs so the row reverts to the `Auth` button
without a reload.
- New `disconnectMCPServerOAuth2` API client method and react-query
mutation that invalidates `mcp-server-configs`.
- Storybook interaction tests: control visibility per auth state, cancel
makes no API call, confirm calls the endpoint once, failed disconnect
keeps the dialog open.
- Hardened `TestMCPServerConfigsOAuth2Disconnect`: seeded tokens flip
`auth_connected`, disconnect only removes the calling user's token, and
repeat disconnect stays idempotent.
The endpoint removes the token stored in Coder; it does not revoke the
upstream OAuth grant, so the UI copy says "disconnect" rather than
"revoke".
Validated with the targeted Go test, Storybook tests (51 passed), tsc,
biome, the react-compiler check, and a manual dogfood run (seeded token,
disconnect/cancel/reconnect flows verified in the UI).
> This PR was authored by Mux, an AI coding agent, on Mike's behalf.
Implements:
https://linear.app/codercom/issue/AIGOV-517/add-ui-for-bedrock-mantle
Follow-up to https://github.com/coder/coder/pull/26745, which added AWS
Bedrock **mantle** support to the backend and modeled it as a `protocol`
field on the Bedrock provider settings.
## What changed
- Adds a **Protocol** selector to the Bedrock provider form: InvokeModel
(default) or Mantle.
- The form is now protocol-aware:
- **Model** / **Small-fast model** fields are shown only for
InvokeModel. Mantle is a passthrough (the client sends the model at
request time), so the fields are hidden and omitted from the saved
settings.
- **Endpoint** validation, placeholder, and hint switch per protocol.
Mantle requires a `https://bedrock-mantle.{region}.api.aws/anthropic`
URL; InvokeModel keeps the
`https://bedrock-runtime.{region}.amazonaws.com` shape. The `/anthropic`
suffix is required for mantle because the SDK appends `/v1/messages` to
the base URL.
## Manual Testing
Tested the following scenarios:
1. Creating a new Mantle provider — works.
2. Creating a new InvokeModel provider — works.
3. Verifying that an existing InvokeModel provider continues to work.
4. Upgrading an existing InvokeModel provider to Mantle — works.
## Screenshots
### InvokeModel
<img width="1085" height="474" alt="image"
src="https://github.com/user-attachments/assets/4c36b9b0-0eb8-4b17-80b7-e2112325f8d1"
/>
### Mantle
<img width="1082" height="357" alt="image"
src="https://github.com/user-attachments/assets/e8caef26-83d1-4b7e-8f96-79079949e8f1"
/>
---------
Co-authored-by: Jake Howell <jake@hwll.me>
## Summary
During the template builder compose flow, the transition into the
template
files page briefly flashed the wizard form again before the files page
settled.
Root cause: the loader was gated on `createMutation.isPending`. When the
compose
POST resolved, `isPending` flipped to `false` in the same commit that
`onSuccess` navigated to the files page, so `TemplateBuilderPageView`
repainted
the wizard form for a frame before the route change committed and
unmounted it.
Fix: keep the loader mounted while the mutation is pending **or**
succeeded
(`createMutation.isPending || createMutation.isSuccess`). Since
`onSuccess`
always navigates away, the loader stays until the component unmounts, so
the
form never reflashes. The error path is unaffected (`isSuccess` stays
false).
Resolves DEVEX-561.
https://github.com/user-attachments/assets/6d5ec04e-9f97-4864-bd18-e1e75055f079
## Scope
Targets the wizard form reflash only. Two adjacent items were identified
during
investigation and intentionally left out:
- The destination files page briefly shows its own `<Loader />` while it
fetches
template files. Smoothing that requires prefetching before navigation
and is a
larger change.
- The `justCreated` "Awesome, you just created a template!" alert is
cleared by a
mount effect, so it renders one frame then disappears. Arguably its own
bug.
Happy to follow up on either if wanted.
## Testing
- Biome clean on the changed file.
- Manual: build a template through the wizard and confirm the loader
holds
straight through to the files page with no form flash.
## Stack
Stacked on top of #27276 (DEVEX-593).
---
Generated by Coder Agents.
## Summary
The template builder's "Building your template" loader had a progress
bar that
animated 0→100% every 5s with an infinite repeat, so it visibly
restarted over
and over while a template built. It looked broken and was frustrating to
watch.
This replaces the looping fill with a single ease-out fill that
decelerates
toward 90% and holds until the request resolves and the loader unmounts.
Since the loader is intentionally indeterminate and no progress is
streamed to
the browser, this also removes the now-dead `onUpdate` callback plumbing
from
the backend `waitForProvisionerJob` (its only caller passed `nil`).
Resolves DEVEX-593.
https://github.com/user-attachments/assets/6d5ec04e-9f97-4864-bd18-e1e75055f079
## Commits
- `refactor(coderd): drop unused onUpdate callback from
waitForProvisionerJob`
- `fix(site/src/pages/TemplateBuilder): stop build progress bar from
looping`
## Testing
- `go build ./coderd/` passes with the reduced `waitForProvisionerJob`
signature.
- Biome clean on the changed frontend file.
- Storybook: `pages/TemplateBuilder/BuildingTemplateLoader` shows the
bar fill
once and hold, with no restart.
<details>
<summary>Implementation plan</summary>
# DEVEX-593: Stop the build progress bar from looping repeatedly
## Problem
While the template builder composes and imports a template, the FE shows
`BuildingTemplateLoader`. Its progress bar animates from 0% to 100% over
5s
with `repeat: Number.POSITIVE_INFINITY`, so it visibly restarts over and
over.
Users report this looks broken and is frustrating to watch while
waiting.
## Decision (scope)
Minimal fix only: **stop the loop**, plus remove the now-dead `onUpdate`
plumbing from the backend. Since the UI is intentionally indeterminate
and no
progress signal is streamed, the callback serves no purpose and should
be
deleted rather than left as dead code.
### Why not "real sync" now
- `POST /api/v2/templatebuilder/compose/template` is a single blocking
request.
It composes, bundles, inserts the provisioner job, then calls
`waitForProvisionerJob(jobCtx, provisionerJob.ID, nil)` and only
responds once
the job completes.
- The `onUpdate` callback runs server-side only. Nothing is streamed to
the
browser during the wait, so the FE has no progress signal to bind to.
- A provisioner job exposes no numeric percentage. Only status
transitions
(`pending -> running -> succeeded`) and coarse log stages
(`init/plan/graph/apply`) exist. Real sync would require converting the
endpoint to a streaming protocol (SSE/WebSocket) plus FE rework, which
is
disproportionate for this 1-point ticket.
### Keep polling (do not switch to pubsub-block)
The wait could technically block instead of poll: on completion
`CompleteJob`
publishes `ProvisionerJobLogsNotifyMessage{EndOfLogs: true}` on the job
logs
notify channel, so we could subscribe and wait for that message with the
context timeout as a fallback. We deliberately do not do that here:
correctness
would require subscribe-before-completion plus an initial DB completion
check to
avoid a race, and Postgres LISTEN/NOTIFY is at-most-once (can drop under
load),
so a poll fallback would still be needed. The existing backoff poll
(100ms -> 200ms -> 500ms -> 1s) is simple and robust for a short-lived
synchronous request.
## Approach
Replace the looping fill with a single, non-repeating ease-out fill that
decelerates and approaches (but never reaches) ~90%, holding there until
the
request resolves and the loader unmounts. This reads as continuous
forward
progress for an unknown-duration operation and never restarts. The
floating-icon
animation is intentional ambient motion and is not in scope.
## Out of scope
- Any behavioral change to how the endpoint waits (it still blocks on
the job).
- Streaming real job progress to the browser.
- Changes to the floating-icon animation.
</details>
---
Generated by Coder Agents.
In the agents sidebar, show "Archived chats" instead of "Chats" when the
archive filter is set to `archived`. The heading reads the existing
`sidebarFilters.archiveStatus` state, the same condition already used
for the archived empty-state message.
---
_This PR was opened by [Coder Agents](https://coder.com/) on behalf of
@tracyjohnsonux._
---------
Co-authored-by: Coder Agents <noreply@coder.com>
closes DEVEX-575
There are some screen widths / base template names where the official
badge will break lines by itself as an orphan (see Google Compute Engine
(Linux) in screenshot below). However, I personally prefer this over the
current flex layout (compare with the screenshot in original issue)
<img width="1840" height="1191" alt="image"
src="https://github.com/user-attachments/assets/d5bee310-0202-4d45-aef7-767e457be051"
/>
Unfortunately ` ` only works if the entities on either side are
both text--not text on the left + image on the right, which is what we
have here.
^One alternative to prevent orphan badges is to split the base template
name by words, then wrap the last word + the official badge in a span
with `white-space: nowrap;`. But that feels a little overwrought to me;
I like the clarity of continuing to just show `{name}` in the JSX. What
do you guys think?
The test-e2e job could flake while Playwright waited for its webServer
to become ready. CI already built site/e2e/bin/coder, but that binary
was built before pnpm build and without the embed tag, so Playwright
ignored it and started coderd with go run -tags embed instead. That put
Go compilation, including frontend asset embedding, inside Playwright's
webServer timeout.
Local runs had a similar ordering trap. pnpm playwright:test invoked
Playwright directly, and Playwright starts webServer before globalSetup,
so the existing preflight build could not guarantee a binary existed
before webServer startup.
Build the E2E coder binary with the embed tag and make it depend on
site/out/index.html. In CI, build the frontend first, then build the E2E
binary, then run Playwright. For local runs, have pnpm playwright:test
and pnpm playwright:test-ui run make site/e2e/bin/coder before invoking
Playwright.
Playwright now starts the prebuilt site/e2e/bin/coder binary and waits
on the public /healthz endpoint instead of an authenticated API
endpoint. This keeps compilation outside the webServer readiness timeout
and makes the readiness probe quieter and more direct. I moved to
/healtz because the existing endpoint was returning 401, which still
means “server is up,” but it is a strange readiness signal. /healthz is
explicitly public and returns 200 OK once the HTTP server is accepting
requests.
Caveat: running pnpm exec playwright test
--config=e2e/playwright.config.ts directly bypasses the package script
and therefore bypasses the prebuild. Use pnpm playwright:test or make
test-e2e for the supported local flow.
Because the site package scripts now call make, add make to knip's
ignored
system binaries.
Tests:
- pnpm lint:types
- pnpm exec playwright test --config=e2e/playwright.config.ts --list
-
PATH="/opt/homebrew/opt/make/libexec/gnubin:/opt/homebrew/opt/gnu-getopt/bin:$PATH"
make gen/mark-fresh
-
PATH="/opt/homebrew/opt/make/libexec/gnubin:/opt/homebrew/opt/gnu-getopt/bin:$PATH"
pnpm playwright:test --list
<!--
If you have used AI to produce some or all of this PR, please ensure you
have read our [AI Contribution
guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING)
before submitting.
-->
Blocked turns from a provider's content filter (Anthropic's `refusal`
stop reason with empty content) previously ended silently on the
"Thinking" spinner. They now end as a terminal `content_filter` error
that renders as a "Response blocked" message with the provider's
category and explanation.
<img width="888" height="335" alt="image"
src="https://github.com/user-attachments/assets/cef85a59-4091-4e62-9d45-1eb06748db48"
/>
Closes CODAGT-611
Follow-ups will involve implementing fallbacks, but this alone is pretty
important
- Regenerates `prices.json` from models.dev. The seeder only upserts, so
existing deployments keep delisted models.
- Generate the frontend known-models catalog instead of hand-writing it.
`make gen/aibridge-prices` fetches models.dev once
- Moved patches to model definitions to separate `overrides.jq` which
handles both `claude-sonnet-4-5` 200k context and 'aliasing' Fable 5
as Mythos 5.
- Editorial choices of selection, order, aliases, and reasoning defaults
live in `curation.json`.
- Adds golden join tests with one error case per validation, a
no-network drift test comparing curation to the checked-in artifact, and
pinned invariants for the Anthropic thinking-mode split (the wrong side
returns HTTP 400) and the sonnet-4-5 context pin.
Adding a model is now one `curation.json` entry plus `make
gen/aibridge-prices`, assuming it is present on models.dev.
> This PR was authored by Coder Agents on Cian's behalf.
---------
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
## 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