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
Registers an OSC 52 handler on the xterm.js terminal parser so that
programs like tmux can copy text to the browser's system clipboard via
escape sequences (e.g. `printf "\\033]52;c;$(echo -n 'Coder is Cool' |
base64)\\a"`).
The handler decodes the base64 payload and writes it using the existing
`copyToClipboard` utility, which provides an HTTP fallback for insecure
contexts. Clipboard read queries (`?`) are ignored since responding
would require writing back to the PTY.
No new dependencies are needed; xterm.js 5.5.0 already exposes
`terminal.parser.registerOscHandler`.
Closes https://github.com/coder/coder/issues/16577
Generated by Coder Agents on behalf of @aqandrew.
## relevant context from agent chat, summarized by me
ref DEVEX-465
Installing
[@xterm/addon-clipboard](https://npmx.dev/package/@xterm/addon-clipboard)
would have been another way to implement this feature. This would follow
established addon-loading patterns in `WorkspaceTerminal`:
https://github.com/coder/coder/blob/612b6d4e95ac4eb1c4227dfd5979a04c2de6e600/site/src/modules/terminal/WorkspaceTerminal.tsx#L232-L241
However, registering a custom handler instead has a few advantages re:
security and flexibility:
- security
- **ignoring OSC 52 with clipboard read queries** (`payload === "?"`)
prevents processes in the workspace from reading the user's clipboard
without consent
- flexibility
- **ignoring OSC 52 with invalid base64** -- If the addon catches an
error while decoding base64, an empty string will be copied to the
clipboard, i.e., the clipboard contents get cleared, which would be
surprising/frustrating to users. The custom handler doesn't write
anything to the clipboard in this case.
- **ignoring OSC 52 with missing separator** -- The addon returns true
in this case (marks the OSC 52 as handled); the custom handler returns
false (doesn't mark the OSC 52 as handled)
The approach/tests seem sound to me. I would just ask that someone among
reviewers manually test this with tmux, since I'm not a tmux user 🙏🏽
Radix's FocusScope defers its unmount event dispatch and focus restore
with setTimeout(0). The cleanup() in the shared afterEach schedules that
timer, and for the last test in a file it could still be pending when
vitest tears down the jsdom environment. The callback then constructs a
CustomEvent from Node's built-in constructor instead of jsdom's, and
jsdom rejects the dispatch with "parameter 1 is not of type 'Event'" as
an unhandled error, failing the run.
Await one timer turn in afterAll, while the jsdom environment is still
alive, to deterministically drain the 0ms timers scheduled by cleanup().
Timers with the same delay run in FIFO order, so this is not a race.
Fixescoder/internal#1613
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
Stack:
1. #26575 `fix(site/e2e): close mock external-auth servers in teardown`
2. #26793 `fix(site/e2e): accept 404 from external auth reset hook` ←
this PR
3. #26795 `fix(site/src): refresh provider state after device-flow
exchange`
4. #26798 `fix(site/e2e): reset both providers in external auth hook`
5. #26648 `chore(site/e2e): re-enable externalAuth suite`
`deleteExternalAuthByID` used to be inverted: `sql.ErrNoRows` (link
doesn't exist for this user/provider) fell through to the `500` path,
while non-`ErrNoRows` DB errors went to `httpapi.ResourceNotFound`.
#19775 (Sep 2025) refactored it to return `404` for not-found and `500`
for real DB errors, which is the contract you'd expect.
The relevant lines from #19775 in `coderd/externalauth.go`:
```diff
- err := api.Database.DeleteExternalAuthLink(ctx, ...)
+ link, err := api.Database.GetExternalAuthLink(ctx, ...)
if err != nil {
- if !errors.Is(err, sql.ErrNoRows) {
+ if errors.Is(err, sql.ErrNoRows) {
httpapi.ResourceNotFound(w)
return
}
httpapi.Write(ctx, w, http.StatusInternalServerError, ...)
return
}
```
`resetExternalAuthKey` in `site/e2e/hooks.ts` still treats `500` as the
not-found code, so the first `beforeEach` in the externalAuth suite
throws. The suite was skipped at the time #19775 landed (#17235), so
nobody noticed the contract drift until #26648 tried to re-enable it.
This just flips the accepted status codes to `200 || 404` and rewrites
the stale comment. The 401/403/500 paths still surface as failures,
which is what we want.
Refs https://linear.app/codercom/issue/DEVEX-413
Refs https://github.com/coder/coder/pull/19775
<details>
<summary>Why a separate PR</summary>
Keeps the bisection signal clean: #26575 proves the EADDRINUSE flake is
fixed, this PR fixes the hook contract drift surfaced by re-enabling the
suite, and #26648 just flips `.skip`. Squashing into #26648 would
conflate two unrelated fixes.
The CI run on #26648 already confirms the flake fix is doing its job:
`successful external auth from workspace` passes (5.6s) and the
`beforeAll`/`afterAll` mock servers come up and tear down cleanly with
no EADDRINUSE. The only failures are this 404 hook drift.
</details>
This PR adds a new opt-in setting, `CODER_OIDC_REDIRECT_ALLOWED_HOSTS`,
that lets a single Coder deployment complete OIDC login on more than one
hostname. When the allowlist is non-empty, Coder picks the OIDC
`redirect_uri` based on the incoming request's Host header (validated
against the list) instead of always using the static URL derived from
`CODER_ACCESS_URL`. When unset, the (default) behavior is identical to
today.
The motivation is that a single Coder deployment is frequently reachable
via multiple hostnames - for example, an internal hostname for users on
a corporate VPN and a different hostname routed through a zero-trust
gateway for users off-VPN - but OIDC login today only works on whichever
single hostname `CODER_ACCESS_URL` points to, because the `redirect_uri`
sent to the IdP is fixed at server startup. Users who reach the
deployment on any other valid hostname can see the login page but fail
the OIDC callback, since the IdP redirects them back to a hostname they
can't reach (or whose cookies they don't have).
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
Adds a bundled `linear.svg` icon at `site/static/icon/linear.svg` so
template authors can reference Linear as a first-party `/icon/`.
`site/src/theme/icons.json` is regenerated by `make
site/src/theme/icons.json` — the diff is a single-line insertion between
`lakefs.svg` and `lxc.svg`.
## Verification
- `go test ./scripts/gensite -run TestSVGIconAttributes/linear.svg` —
PASS (`width="256"`, `height="256"`, `viewBox="0 0 256 256"`)
- `make lint/site-icons` — PASS
- `go test ./scripts/gensite -count=1` — PASS (full SVG attribute sweep)
Surfaces the server-generated STS external ID on the Bedrock provider
edit form. When a provider assumes a role, the form shows the external
ID read-only with a copy icon and a short note to add it to the target
role's trust policy as an sts:ExternalId condition.
The value is display-only: it is passed to the form as its own prop
rather than as an editable form value, so it is never submitted back.
This matches the backend contract, where the external ID is server-owned
and a changed value is rejected.
Builds on the backend in #26869. Follow-up to #26578.
---------
Co-authored-by: Jake Howell <jake@hwll.me>
Adds the Omnigent SVG icon to the built-in site icon assets so modules
and templates can reference `/icon/omnigent.svg`.
## Validation
- `./scripts/check_site_icons.sh`
- Verified `site/src/theme/icons.json` is sorted and includes
`omnigent.svg`
- Parsed `site/static/icon/omnigent.svg` as valid XML
> 🤖 This PR was created with the help of Coder Agents, and needs a human
review. 🧑💻
---------
Co-authored-by: Jake Howell <jake@hwll.me>
ref DEVEX-567
Fixes a bug I noticed where #26881 claims to have made the template
builder's `SelectionSummary` sidebar sticky-positioned, but the sidebar
position wasn't actually sticky in practice:
## template builder's `SelectionSummary` (top right)
### before
https://github.com/user-attachments/assets/1e9360b5-4b0e-45b4-a859-f8edab51add7
### after
https://github.com/user-attachments/assets/3cee75e9-9813-401e-9922-d2563185f8f5
---
Why I removed `overflow-y-auto` from `#main-content`:
>`position: sticky` resolves against the nearest ancestor scroll
container — any ancestor whose overflow is not visible. `#main-content`
had `overflow-y-auto`, so it _was_ that container for the sidebar. But
because the layout wrapper is `min-h-screen`, `<main>` grows to fit its
content and never actually scrolls — the window scrolls. So the sidebar
was pinning relative to a container that never moves → no effect.
That was necessary for the main goal of this PR, which was to fix sticky
positioning for the template builder's `SelectionSummary` sidebar.
However, removing that style from `#main-content` affected 2 other
sticky-positioned elements within the site:
>Two other pages have sticky elements that, like our sidebar, were
resolving against the non-scrolling `<main>` and were therefore
effectively inert:
>- CreateWorkspacePageView.tsx:397 — `sticky top-5` side panel
>- modules/templates/TemplateFiles/TemplateFiles.tsx:64 — `sticky top-8`
file tree
^Like `SelectionSummary`, these 2 elements hadn't been behaving with
sticky positioning as expected; they would just scroll away past the top
of the screen.
For all 3 of these sticky elements, since their `top` is now relative to
the window instead of `#main-content`, they have to be positioned
farther downward so that they don't get covered by the navbar.
## `TemplateFiles`' `TemplateFileTree` (top left)
### before
https://github.com/user-attachments/assets/ee79ea36-1a47-4b1b-86ef-c65b493ea455
### after
https://github.com/user-attachments/assets/a1b0a949-b5e3-4434-acaa-ab24e82b1669
## `CreateWorkspacePageView`'s "Go back" button (top left)
### before
https://github.com/user-attachments/assets/00b076f6-8e55-4a96-8b92-d33d92c85334
### after
https://github.com/user-attachments/assets/10ae05e1-3112-4dfc-84b4-2c44fee23e06
co-authored with Claude Code
---------
Co-authored-by: Jeremy Ruppel <jeremyruppel@users.noreply.github.com>
## Summary
Add provisioner awareness to the Template Builder wizard: disable the
Create Template button when the selected organization has no
provisioners, and reset the customizations step when navigating back.
## Changes
- Query provisioner daemons for the selected org in
`TemplateCustomizationsStep` and show a warning alert when none are
found
- Track `hasProvisioners` in wizard state via `SET_HAS_PROVISIONERS`
action; `computeCanContinue` disables the Create Template button when
`hasProvisioners === false`
- Add `RESET_CUSTOMIZATIONS` action to clear customization fields (name,
displayName, description, icon, organizationId, hasProvisioners) when
navigating back from the customizations step
- Clear the create mutation error on back navigation via
`onClearCreateError` callback
Follows up on #26935 which added the provisioner warning alert to the
Template Builder.
> 🤖 Generated by Coder Agents on behalf of @jeremyruppel
Implements:
https://linear.app/codercom/issue/AIGOV-495/add-externalid-to-prevent-confused-deputy-problem
When a Bedrock provider assumes an IAM role via STS, the gateway now
generates a unique external ID for it and sends that value on every
`AssumeRole` call. The external ID guards against the [confused deputy
problem](https://docs.aws.amazon.com/IAM/latest/UserGuide/confused-deputy.html)
on cross-account role assumption. Per [AWS's
recommendation](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-user_externalid.html),
the gateway generates and owns the value rather than accepting one from
the operator; that ownership is what makes it effective, since a party
who knows another's external ID can't induce the gateway to send it.
The external ID is server-owned and read-only over the API. It is
generated once, when a provider first has a `role_arn`, and is stable
thereafter. Clients cannot set it: create rejects any supplied
`external_id`, and update rejects a value that differs from the stored
one. An update may echo the stored value back unchanged, so the normal
read-modify-write flow (GET the provider, change a field, PATCH the full
settings object) keeps working. The value is not a secret and is
returned on GET so operators can copy it into the target role's trust
policy as an `sts:ExternalId` condition.
It is persisted in the existing JSON settings blob, so there is no
migration or audit-table change.
## Summary
Show a warning in the Template Builder customizations step when the
selected organization has no provisioner daemons connected. This matches
the existing behavior in the current `CreateTemplateForm`.
## Changes
- Added a `provisionerDaemons` query in `TemplateCustomizationsStep`,
gated on the selected org
- Renders an `Alert` warning with a link to provisioner docs when no
daemons are found
- Warning is informational only and does not block form submission
<details>
<summary>Implementation plan</summary>
The current `CreateTemplateForm` (used for upload/starter/duplicate
flows) shows a warning when the selected organization has no provisioner
daemons connected. The new Template Builder wizard
(`/templates/new/builder`) had an org picker in
`TemplateCustomizationsStep` but did not perform this check.
### What was done
1. Imported `provisionerDaemons` query helper, `Alert`, `Link`, and
`docs` utility into `TemplateCustomizationsStep.tsx`
2. Added a `useQuery` call for provisioner daemons, enabled only when an
org is selected
3. Computed `showProvisionerWarning` (true when the provisioners list is
empty)
4. Added a local `ProvisionerWarning` component rendering the same
warning text and docs link as `CreateTemplateForm`
5. Rendered the warning below the `OrganizationAutocomplete` picker
### Design decisions
- Warning is informational only (does not block submission), matching
the existing form behavior and the TODO rationale: a user may connect a
provisioner without refreshing
- Used the project's `Link` component (`#/components/Link/Link`) instead
of MUI Link
- Kept `ProvisionerWarning` as a local component rather than extracting
to shared, since the component is small (~8 lines)
</details>
> 🤖 Generated by Coder Agents on behalf of @jeremyruppel
<img width="2574" height="1274" alt="Screenshot 2026-07-01 at 3 56
58 PM"
src="https://github.com/user-attachments/assets/45f0d81b-1f9a-4a93-a2d3-5f037e82d1d0"
/>
This PR removes the now-dead direct-routing code:
- Deletes the direct routing implementation.
- Collapses the resolvedModelRoute discriminated union into aiGatewayModelRoute.
- Removes the dead providerKeys cascade.
- Deletes the preferredShortTextCandidates quickgen function.
- Simplifies the advisor override error handling.
- Deprecates the AIGatewayRoutingEnabled deployment option. It is now a no-op so as to not break existing deployments on upgrade.
Once direct routing was gone, the AI Gateway became mandatory for chat, which surfaced gaps in how the product behaves with the gateway disabled:
- Exposes ai-gateway-enabled to the frontend via embedded page metadata.
- Disables the chat composer via the existing AgentSetupNotice when the gateway is disabled, for both new and existing chats.
- Fixes nil/typed-nil chatDaemon panics on startup and shutdown when gateway is disabled.
- Fixes chat WebSocket from retrying the still-gated stream endpoint forever when the gateway is disabled.
Right-clicking **Archive & delete workspace** on an agent chat could
leave the workspace behind without the user noticing. The archive step
ran first and removed the chat from the sidebar, so when the delete
enqueue failed the user lost the surface to retry and the workspace
lingered.
## Fix
- Delete the workspace first, archive the chat second. If the delete
enqueue fails for anything other than 404/410, the chat stays in the
list so the user can retry. 404/410 are still treated as "already gone"
so the archive proceeds.
- Errors are wrapped in `ArchiveAndDeleteError` tagged with `step:
"delete" | "archive"`. The toast branches on the tag: `delete` failures
show an actionable "Open workspace" link, `archive` failures explain the
delete already ran so no manual deletion is needed.
- Both mutation call sites navigate away on `onSuccess` only (previously
`onSettled`), so a delete failure keeps the chat's retry surface
reachable. The confirm dialog still closes on `onSettled`.
- When the archive step fails after a successful delete,
workspace-related caches are invalidated to keep the rest of the app in
sync with the ongoing deletion.
- On successful enqueue, warn when the build response's
`matched_provisioners.count` is 0. That field is populated on `POST
/workspacebuilds`; `job.queue_position` / `job.queue_size` are not.
> 🤖 This PR was generated by Coder Agents on behalf of @johnstcn.