> 🤖 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>
## 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"
/>
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
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 🙏🏽
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>
The provider type already lives authoritatively in ai_providers.type,
reachable on every active row through ai_provider_id, which the
chat_model_configs_ai_provider_required_when_active CHECK makes
mandatory. The stored provider string was a denormalized copy the system
kept in sync with a startup backfill and no longer needs.
Every surface now derives provider type from the linked ai_providers
row. Telemetry is the one exception: it keeps emitting provider, now
sourced from ai_providers.type via a JOIN, so the BigQuery column and the
Nexus dashboards that read it are unaffected. The experimental HTTP/SDK
response drops provider and makes ai_provider_id required, since those
endpoints return only active configs; consumers resolve provider type
from ai_provider_id and the AI providers listing.
This ships in a single release with no compatibility window: production
reads the table via SELECT *, so a pre-drop binary fails config reads the
moment the column is gone. Operators must scale to zero before upgrading,
and there is no rollback.
Closes CODAGT-599
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
<img width="628" height="353" alt="image"
src="https://github.com/user-attachments/assets/16fbf58f-282a-4629-b0de-45160117adb6"
/>
Linear:
[DES-22051](https://linear.app/codercom/issue/DES-22051/adjust-workspace-list-icon-and-agent-link-affordance)
## Problem
[#23374](https://github.com/coder/coder/pull/23374) swapped the
underlying `Tooltip` for a `Popover` under the `HelpTooltip` →
`HelpPopover` rename, turning the trigger from a hover surface into a
click surface.
On the workspaces list, `WorkspaceOutdatedTooltip` is rendered inside a
`<TableRow>` wired up by `useClickableTableRow`, whose `onClick`
navigates to the workspace page. Other interactive children in the same
row already stop propagation (checkbox, actions cell, agent badge). The
outdated tooltip didn't, so clicking the info icon bubbled up to the
row's `onClick` and navigated away before the popover could open.
## Fix
Stop click and keydown propagation on both trigger variants
(`HelpPopoverTrigger asChild` span and `HelpPopoverIconTrigger`) in
`WorkspaceOutdatedTooltip`. The popover still opens because Radix
composes its own click handler on top of the user-provided one via
`composeEventHandlers`; `stopPropagation()` does not set
`defaultPrevented`, so Radix's toggle still runs.
## Regression coverage
Added an `InsideClickableRow` story that mounts the tooltip inside a
`useClickableTableRow` row with a tracked `onRowClick`, clicks the
trigger, asserts the popover dialog opens, and asserts `onRowClick` was
not called. Verified locally that the new story fails on `main` (1 call
to `onRowClick`) and passes with this fix.
<details>
<summary>Decision log</summary>
- Fix lives on the component (rather than at the `WorkspacesTable.tsx`
call site) so every consumer is safe by default. The popover is
interactive in a way the parent shouldn't have to know about, and the
existing call site in `TaskPage.tsx` is unaffected because its parent
has no click handler.
- `onKeyDown` propagation is also stopped so the trigger keeps working
when activated via keyboard, and to satisfy
`lint/a11y/useKeyWithClickEvents` on the `<span>` trigger variant.
- Considered pushing the guard down into the `HelpPopoverIconTrigger` /
`HelpPopover` primitives so every consumer is covered — left as a
separate follow-up since today only `WorkspaceOutdatedTooltip` is
embedded in a `useClickableTableRow` row, and a blanket change would
need a broader audit.
</details>
Hides UI, CLI and API related to AI Gateway key management +
`/api/v2/ai-gateway/serve` endpoint.
API endpoints and CLI commands are still working they are just not
visible.
Adds the current user's AI spend progress to the top navbar avatar
dropdown when /api/v2/users/me/ai/spend reports a configured spend
limit. The shared dropdown content accepts an optional profileExtra
slot so the Agents sidebar can opt in later without changing the
default sidebar UI.
Introduces a temporary site API type and a React Query helper for
AIGOV-473 that refetches each time the dropdown opens, plus shared
budget progress helpers used by both the new dropdown bar and the
existing Agents usage indicator. The shared AIBudgetUsage component
moves to site/src/components so the dropdown and group budget UI
format spend identically, including the unlimited case. The avatar
border polls the spend endpoint and is colored by severity while the
dropdown is closed.
Closes AIGOV-473
Configuring only a GitHub Copilot provider left the Agents page stuck on
"set up a provider then add a model", even with a provider and models
configured. The catalog dropped any provider type that NormalizeProvider
did not recognize, so a Copilot-only deployment looked identical to an
empty one and never unlocked the page.
The Agents harness cannot use Copilot: it needs a per-request token only
an official Copilot client can mint, and the harness is not one. Instead
of dropping such providers, the catalog now reports them as unsupported
so the UI can explain the dead end and point elsewhere, rather than ask
for setup that already happened. The providers stay usable through the
AI Gateway proxy.
Support is derived from the provider type, not stored, so there is no
migration. codersdk.IsAgentsUnsupportedProviderType is the single source
of truth, consulted by the chatd catalog and, through the generated
AgentsUnsupportedProviderTypes list, the frontend.
The diff also carries unrelated modernization of nearby db2sdk and
chatprovider helpers (slices.SortFunc, strings.Cut, range-over-int).
Closes CODAGT-627
Refs CODAGT-256
Refs CODAGT-682
Groups the agent-related AI settings pages under a new **Coder Agents**
parent in the sidebar, with a continuous left rule connecting the
children and an active-segment indicator that lights up the rule where
the current sub-item sits.
The new nav order:
- AI Governance
- AI Gateway keys
- Providers
- Coder Agents
- Models
- MCP servers
- Templates
- Spend
- Instructions
- Lifecycle
All target pages already exist on main (Danielle's recent migrations of
Models, MCP servers, Templates, Instructions, Lifecycle, Spend, and
Coder Agents into AI Settings). This PR only changes the sidebar visual
structure: the children move into an indented group with a `border-l
border-l-border` rule, and the active child paints a
`border-l-content-primary` segment over that rule via `-ml-px` so the
rule and indicator share a column instead of stacking.
<details>
<summary>Design notes</summary>
Concept 1 from the earlier exploration: always-expanded with indents,
the parent is its own page. Chosen because it adds no expand/collapse
state, no "which child is the default" question, and no animation work;
the parent reuses the existing nav-item, and the children sit in a
wrapper `div` with a left rule. The site bundle ships without Tailwind's
preflight, so the wrapper and sub-item borders are paired with
`border-solid` to actually paint, matching the pattern already in
`Sidebar.tsx`.
</details>
---
_This PR was prepared by Coder Agents on behalf of @tracyjohnsonux._
Move the providers routes into a dedicated providers sub-tree: `/ai/settings/providers`, `/ai/settings/providers/add`, and `/ai/settings/providers/:providerId`.
The old `/ai/settings/:providerId` and `/ai/settings/add` URLs are
removed without backward-compatibility redirects. Bookmarked or shared
links to these paths now return a 404. Creating a provider with id `models` (although unlikely) made it impossible to edit it due to a conflict with the static models route.
## Description
Moves frontend routes from `/aibridge` to `/ai-gateway` and adds client-side redirects so existing bookmarks and deep links continue to work.
## Changes
- Move React Router paths from `/aibridge` to `/ai-gateway`
- Add `<Navigate>` redirects from `/aibridge`, `/aibridge/sessions`, and `/aibridge/sessions/:sessionId`
- Update `navigate()` calls and `Link` components to use new paths
Closes https://linear.app/coder/issue/AIGOV-233
> Generated with the assistance of Coder Agents (@ssncferreira)
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
Replaces the basic `animate-in fade-in zoom-in duration-200` on the
inbox unread badge with a bouncier pop using an overshoot easing.
The badge now pops in from `scale(0)` with `cubic-bezier(0.34, 1.36,
0.64, 1)` over 500ms, giving it an elastic overshoot that makes it feel
alive. Uses `tailwindcss-animate` utilities (`animate-in zoom-in-0
fade-in-0`) with just one arbitrary property for the easing. Conditional
rendering is preserved as before.
Also adds a `BadgeTransition` story with toggle buttons for easy visual
verification.
<img width="150" height="150" alt="preview-notification-animation"
src="https://github.com/user-attachments/assets/70331cd2-c2e2-4fd7-8f9e-edfe876e2bab"
/>
Updates the icons in the **AI add-on** column for the admin tables on
`/deployment/users` and `/organizations/.../members`.
- Not consuming a seat: `XIcon` → muted em-dash
(`text-content-disabled`).
- Consuming a seat: `CircleCheckIcon` → plain `CheckIcon` from
`lucide-react` (no circle, still `text-content-success`).
Both tables share `site/src/modules/users/AISeatCell.tsx`, so the
single-file change covers `UsersTable.tsx` and
`OrganizationMembersTable.tsx`.
## Screenshot
Users admin table on a local dogfood build of this branch
(`/deployment/users`):
<!-- Drag-and-drop the screenshot here to attach it to the PR. -->
<details>
<summary>Notes</summary>
- The em-dash is rendered via the JSX entity `—` rather than a
literal `U+2014` character, so it does not trip `make lint/emdash` (see
`AGENTS.md` / `scripts/check_emdash.sh`). The browser still renders an
em-dash.
- Kept the existing `aria-label`s (`Consuming AI seat` / `Not consuming
AI seat`) and added `role="img"` on the em-dash span so screen readers
still announce the state.
- No callers other than `UsersTable.tsx` and
`OrganizationMembersTable.tsx` use `AISeatCell` (verified via search).
</details>
> Opened by Coder Agents on behalf of @tracyjohnsonux.
fixes#22420
ref DEVEX-369
ref DEVEX-269
The bug on `CreateWorkspacePage`, where clicking one external auth
provider login button disabled all providers' login buttons, was caused
by providers all sharing a single polling status (`"idle" | "polling" |
"abandoned"`) in the `useExternalAuth` hook.
## changes
- Instead of setting one status across all providers, the polling status
in `useExternalAuth` is now tracked for each provider in a record whose
keys are the providers' IDs.
- The biggest diff is a new Storybook file
CreateWorkspacePage.stories.tsx which reproduces the bug behavior from
the issue.
- Until now we've only had CreateWorkspacePageView.stories.tsx, which
isn't able to model the user interactions / API responses needed to
verify the bugfix. This file is unchanged.
- Also deletes `CreateWorkspacePage`'s `useExternalAuth` hook in favor
of the global `useExternalAuth` hook (see #26310)
(co-written with Coder Agents)
ref DEVEX-449
## before
In `AgentDevcontainerCard`, the devcontainer's display name truncated
with an ellipsis--but only when the viewport is <768px (parent has
`md:w-full`), which is also when the display name width gets capped at
320px (`max-w-xs`). At larger screen sizes, the display name overlaps
with the rebuild button to the right.
https://github.com/user-attachments/assets/47af30a2-f446-4b5f-bf63-24afb0532b19
## after
Now, the devcontainer display name fills up as much width as possible at
all screen sizes (not just >=768px), and it also gets truncated with an
ellipsis at all screen sizes.
https://github.com/user-attachments/assets/68579438-6fb5-40ea-93cf-c3814dfaac1a
---
(If we have a way to automate resizing screens in Storybook, lmk!)
The trick is to set a `min-width` on the text we want to truncate, see
this CSS Tricks article: https://css-tricks.com/flexbox-truncated-text/
I changed `max-w-xs` to `min-w-[284px]`. In addition to constraining the
display name's width (see "before" above), the `max-width` caused the
rebuild button to wrap to the next line if the screen is too narrow to
show the display name at that width. This behavior on small screens is
preserved. 🙂 However, since the display name's width isn't constrained
anymore, it's written out fully if possible, which causes the rebuild
button to wrap lines on wider screens if the display name is very long.
Adds workspace app and forwarded port tabs to the AgentsPage right panel, so app previews and port views can sit alongside terminals while working in the chat view.
- The new add-tab dropdown lists the agent's apps and a ports submenu: embeddable apps open as iframe tabs, command apps open as renamed terminal tabs running their command, and ports open in a new port preview panel served through the wildcard access URL.
- Tabs persist per chat and are validated against the current workspace state: tabs whose app disappears, stops being embeddable, or whose agent no longer exposes the port forwarding helper are hidden rather than deleted, and reappear if the workspace exposes them again.
- A shared `usePortsData` hook keeps the add-tab control, workspace pill, and existing `PortForwardButton` on the same port queries and refresh cadence.
- ~~App and port tabs are limited to `devel` builds for now; terminal tabs remain generally available. This was done in favour of adding a backend experiment for a frontend-only feature.~~ TODO: I'm just gonna switch to a backend experiment.
Closes CODAGT-346
There was inconsistency with what the form showed and what actually was
sent to the backend. I opted to make it so that explicitly blank values
are always sent rather than have blank values silently changing to the
default value.
> Vibe-coded using Coder Agents, author with limited frontend knowledge,
manually tested.
Adds an `AI Gateway Keys` page under `Admin settings > AI > AI Gateway
Keys` for key management of keys used by standalone AI Gateway replicas
to authenticate into `coderd`.
The page is shown to users with `viewAIGatewayKeys` permission and a
Premium license with AI Gateway enabled.
Adds Storybook coverage.
---------
Co-authored-by: Jake Howell <jacob@coder.com>
Extracts the workspace app iframe, wildcard warning, and workspace-app
helper functions out of TaskPage into shared `site/src/modules/apps`
modules. Existing agent and app lookups in the task chat helpers,
download-logs dialog, and workspaces table now route through the shared
`workspaceApps` helpers instead of duplicating resource-flattening
logic. The extracted frame preserves the existing preview-only toolbar
behavior, and its open-in-new-tab link gains `rel="noreferrer"` to
harden against tabnabbing.
Relates to CODAGT-346
## Summary
Fixes [CODAGT-415](https://linear.app/codercom/issue/CODAGT-415).
Right-clicking selected text in the web terminal on Windows (and Linux)
showed
the browser's image actions ("Copy image", "Save image as") instead of
copy/paste. The terminal uses xterm.js with the canvas/WebGL renderer,
so the
underlying element is a `<canvas>`, which Chromium and Firefox treat as
an
image. xterm.js tries to retarget the menu by moving a hidden textarea
under
the cursor, but on Windows and Linux the browser's own non-native
context menu
locks onto the canvas before that workaround lands.
## Change
Wrap the terminal in the shared Radix `ContextMenu` so right-click shows
a
custom **Copy** / **Paste** menu instead of the browser default:
- **Copy** reuses the existing copy-on-select clipboard path
(`getSelection()`
+ `copyToClipboard`). It is disabled when there is no selection.
- **Paste** reads the clipboard and uses xterm's `paste()`, which
respects
bracketed-paste mode.
- The menu is gated to non-macOS (`disabled={isMac()}` on the trigger).
macOS
renders native context menus that already expose working copy/paste
across
Chrome, Firefox, and Safari, so its default is left untouched.
## Platform scope
| Platform | Behavior |
| --- | --- |
| Windows (Chromium / Firefox) | Custom Copy/Paste menu (fixes the bug)
|
| Linux (Chromium / Firefox) | Custom Copy/Paste menu |
| macOS (Chrome / Firefox / Safari) | Native menu preserved (already
works) |
## Testing
- `TerminalPage.test.tsx`: on non-macOS, right-click suppresses the
native menu
and shows the Copy/Paste menu; on macOS the native menu is preserved.
- `TerminalPage.stories.tsx`: new `RightClickMenu` story opens the menu
via a
`play` function for real-browser and visual coverage.
- `tsc`, `biome`, and `make pre-commit` (gen/fmt/lint/build) pass
locally.
<details>
<summary>Decision log</summary>
- The issue was originally reported as Windows-only. Hands-on testing
confirmed
macOS is not affected: Chrome, Firefox, and Safari on macOS all show a
working
copy/paste menu. The difference is the menu implementation: macOS uses
native
OS context menus (which pick up xterm's repositioned textarea), while
Chromium/Firefox on Windows and Linux draw their own menu that targets
the
`<canvas>` directly.
- Root cause is the canvas/WebGL renderer plus the unreliability of
xterm's
textarea-repositioning workaround on non-native menus, not the operating
system itself.
- A custom menu (rather than just `preventDefault`) was chosen so users
keep an
explicit copy/paste affordance on the affected platforms. A bare
`preventDefault` removes the menu entirely.
- Scope is gated to non-macOS to avoid regressing the working native
menu on
macOS. Rejected alternatives: suppressing/replacing on all platforms
(regresses
macOS), and Windows-only (misses Linux, which shares the same non-native
menu).
</details>
---
Generated by Coder Agents on behalf of @jaaydenh.
#26124 introduced a regression on `main`: `AgentRow ›
NonStartupScriptError` fails because the refactor replaced
`hasAgentIssues` (which covered both connectivity and script issues)
with `hasConnectivityIssues` only in the `showLogs` condition. For a
`ready` agent with a failed script but no connectivity issues,
`showLogs` becomes false, logs never load, and the failed script tab
never renders. The PR's own behavior table said logs should still
auto-open in this case — it was an implementation oversight, not an
intentional change.
Fix by including `hasScriptIssues` in the `showLogs` condition alongside
`hasConnectivityIssues`, restoring the auto-expand behavior from #25442
without touching connectivity badge styling.
> **Note:** This reached `main` undetected because `test-js` in CI only
runs `--project=unit`; the Storybook interaction tests
(`--project=storybook`) that caught this are not a required check. When
Chromatic is switched off, `--project=storybook` should be added to the
required gate.
Refs #26124, #25442
Renames all user-visible "AI Bridge" strings to "AI Gateway" in the
browser UI as part of the AI Bridge to AI Gateway rebrand (AIGOV-233).
Changes:
- Page titles and headings ("AI Bridge Logs" to "AI Gateway Logs",
`pageTitle` calls)
- Help popover title, description, and doc link
- Setup alert title, description, and doc link
- Back-link tooltip on session threads page
- AI add-on help popover text
- License entitlement warning shown in the dashboard banner
Doc links updated from `/ai-coder/ai-bridge` to `/ai-coder/ai-gateway`
(directory already exists).
This PR intentionally does not rename internal identifiers,
file/directory names, API endpoints, routes, CLI help, deployment config
options, or generated types. Those will be tracked separately.
Related to: https://linear.app/codercom/issue/AIGOV-233
> Generated by Coder Agents on behalf of @ssncferreira
- Adds a dynamic terminal tab model to the Coder Agents right panel so
users can open, switch between, and close multiple Web Terminal sessions
for the same agent.
- Each terminal tab generates its own UUID reconnect token and is
persisted per agent in local storage, so tabs and their PTY sessions
survive reloads.
- New terminal tabs are auto-labelled (`Terminal 2`, `Terminal 3`, ...),
filling the lowest free number.
- The built-in Terminal tab is now closeable; closing it hides it and
persists that choice per agent, and it can be restored from the add-tab
control.
- Only the active terminal (and a tab that is mid-activation) mounts the
expensive xterm/WebSocket/PTY resources; a freshly hidden terminal stays
warm for 30s to keep quick tab toggles instant, then detaches to bound
resource usage instead of capping the tab count.
- Inactive terminals stay laid out but hidden, and a newly opened
terminal is not activated until it reports ready (or 100ms), avoiding
the narrow refit and blank-frame flicker when switching between terminal
canvases.
- The right-panel tab bar scrolls horizontally with chevron controls
when tabs overflow, keeping everything on a single row.
- Per-agent right-panel tab and hidden-terminal state is cleared from
local storage when a chat is archived or deleted.
- Adds unit tests for the tab utilities, persistence helpers, and the
warm/detach lifecycle hook, plus Storybook play-function coverage for
the tab and terminal components.
Relates to CODAGT-346
Surfaces the org's `default_org_member_roles` inside the org members role editor. These roles are implied, not physically assigned to any member. Just like the `member` role.
Refs #25936.
Adds a configurable per-org default member role set. Unioned into each member's effective roles at read time.
<sub>with Coder Agents on behalf of @Emyrk.</sub>
fixes DEVEX-402
Alternative to #26077, which aimed to address this flake in
UsersPage.stories.tsx only:
>Unable to perform pointer interaction as the element has
`pointer-events: none`
In the original PR I was going to "fix" the flake by adding
`pointerEventsCheck: Never` like we have in 2 other tests. However,
resetting `pointer-events` during `beforeEach` in our global Vitest
setup removes the need for that `pointerEventsCheck` workaround. 🙂
tl;dr: Each of these 3 tests were failing because of an immediately
preceding test which opens a Radix Dialog, setting `pointer-events:
none` on the body element.
Co-written with Coder Agents. Relevant chat responses:
>**The actual root cause is story isolation.** The previous story
(`UpdateUserRoleSuccess`) opens a Radix Dialog, which triggers Radix's
`DismissableLayer` to set `document.body.style.pointerEvents = "none"`.
When that story completes, Storybook unmounts the React tree, but the
cleanup of `body.style.pointerEvents` is tied to a `useEffect` cleanup
in `DismissableLayer`. If Storybook begins the next story's play
function before the browser has fully flushed the previous unmount
cycle, `document.body.style.pointerEvents` is still `"none"`. The "Open
menu" button, which is a plain table button with no Radix modal layer
above it providing `pointer-events: auto`, inherits `"none"` from body,
and the first click fails.
>
>**Why it's flaky:** The Storybook Vitest plugin
(`@storybook/addon-vitest`) runs stories sequentially in the same
browser page. When `UpdateUserRoleSuccess` finishes, React unmounts the
tree, and `DismissableLayer`'s `useEffect` cleanup should restore
`body.pointerEvents`. But there's a race between unmount cleanup
completing and the next story's play function starting. Sometimes
cleanup wins (test passes), sometimes the play function wins (test
fails).
>
>**[#26077]'s `pointerEventsCheck: Never`** works but is a workaround.
It doesn't address the leaked state, and `UpdateUserRoleSuccess` itself
could be flaky too (if whatever story precedes it leaves the same stale
state). The other stories in the same file (Suspend, Delete, Activate,
ResetPassword) all open Radix Dialogs too, so any of them could leak
this state to the next story.
> 🤖 This PR was modified by Coder Agents on behalf of Jake Howell.
The agent logs tab was snapping to **Startup Script** whenever
`getAgentHealthIssues` returned _any_ issue, including non-script issues
like:
- agent `status` of `connecting`, `timeout`, or `disconnected`
- lifecycle `shutting_down`, `shutdown_error`, or `shutdown_timeout`
- any script with status `timed_out`, `exit_failure`, or
`pipes_left_open`
So users routinely landed on a filtered Startup Script view (even
mid-connect) and had to click back to **All Logs** to see the full
picture.
### Change
- Default to **All Logs** on mount.
- Once logs stream in, if any script has actually failed _and_ its
source has rendered log entries, auto-select that script's tab. The
visibility check is the same one the tab list uses, so we never point
`selectedLogTab` at a tab that isn't rendered.
- A ref ensures the auto-select fires at most once and never overrides a
manual tab change by the user.
- The failure predicate (`exit_code` truthy or `status` set and not
`"ok"`) is extracted into an `isScriptFailed` helper so the auto-select
and the per-tab error indicator stay aligned.
### Behavior matrix
| Agent state | Before | After |
| ------------------------------------------------------ |
--------------- | ------------------------------ |
| Healthy, scripts running normally | All Logs | All Logs |
| `connecting` / `timeout` / `disconnected` | Startup Script | All Logs
|
| `shutting_down` / `shutdown_error` | Startup Script | All Logs |
| Startup Script failed, has logs | Startup Script | Startup Script |
| Non-startup script failed, has logs (e.g. install) | Startup Script |
The failed script's tab |
| Failed script with **no logs**, other sources have logs| Startup
Script (broken: tab not rendered) | All Logs |
### Test coverage
Four `play` functions on `AgentRow.stories.tsx`, each pinned to a
hardcoded tab name so a predicate regression can't silently pass:
- **`StartError`** — failed script with logs is auto-selected.
- **`StartErrorWithoutFailedSourceLogs`** — failed script with no logs
keeps All Logs active; we never point at an invisible tab.
- **`ConnectingWithStartupLogs`** — connecting agent with no script
failure stays on All Logs (locks in the bug fix for connection-only
issues).
- **`NonStartupScriptError`** — only a non-startup script fails (Startup
Script is OK); the auto-select tracks the failure, not position or
display name.
Each play function was verified to actually run by deliberately failing
the assertion once and confirming the test errored.
<details>
<summary>Decision log</summary>
The shape evolved during review:
1. First pass: collapse to `useState("all")`. Reviewer noted this lost
the auto-jump for legitimate failures.
2. Second pass: `useState(failedSourceId ?? "all")`. Codex flagged that
`failedSourceId` could point to a tab with no rendered entries, breaking
the Logs panel.
3. Final pass (this PR): `useState("all")` + `useEffect` + ref.
Auto-jump only fires once, only when the failed source has rendered
logs, and never overrides a manual selection.
Deferred from coder-agents-review feedback:
- **DEREM-5**: `agent.log_sources.find(...)` can disagree with the tab
bar's sort order when multiple non-startup scripts fail concurrently.
Low probability, one click to recover. Worth a follow-up if it becomes a
real complaint.
</details>
### Verification
- `pnpm check` (biome) — clean
- `pnpm lint:types` (tsc) — clean
- `pnpm vitest run --project "storybook (chromium)"
src/modules/resources/AgentRow` — 28/28 passed
---------
Co-authored-by: Atif Ali <atif@coder.com>
Co-authored-by: Jeremy Ruppel <jeremy.ruppel@gmail.com>
The descendant selector `[&_img]:size-icon-sm` in `menuItemClass` was
matching the `<img>` nested inside `<Avatar>` on the User settings row
of the mobile menu, pinning the avatar image to `1.125rem` instead of
letting it fill its Avatar container. The same descendant rule also
applied to nested SVGs.
Switch both rules to direct-child selectors (`[&>svg]`, `[&>img]`),
matching the symmetric `shrink-0` rules already next to them and the
same pattern used in `Button.tsx`. Menu items that contain nested
`<img>`s already set their own sizes explicitly (`w-4 h-4` in
`ProxySettingsSub`, `!size-3.5` in `WorkspacePill`), so this only stops
overriding components like `Avatar` that manage their own internal
sizing.
Also removes the now-dead `[&_img]:w-full [&_img]:h-full` workaround in
`UserDropdownContent.tsx` that fought the same bug per-item; that menu
item no longer renders an `<img>`, and the root fix makes the workaround
unnecessary anyway.
Refs
[CODAGT-552](https://linear.app/codercom/issue/CODAGT-552/mobile-menu-user-avatar-image-is-undersized-inside-its-container)
<details>
<summary>Investigation notes</summary>
**Repro (before):** mobile viewport, user has an image avatar set, open
hamburger menu, observe the avatar in the User settings row is smaller
than its bordered square.
**Root cause:** `site/src/components/DropdownMenu/menuClasses.ts` set
`[&_img]:size-icon-sm` using a Tailwind descendant selector (`_`). That
matches every `<img>` *inside* a menu item, including the one nested
inside `<AvatarPrimitive.Root>`. The Avatar's inner image is supposed to
be `aspect-square size-full object-contain` and inherit its size from
the Avatar root (`--avatar-default` ≈ 24px by default), but the
descendant rule overrides that and pins it to 1.125rem.
**Prior workaround:** `UserDropdownContent.tsx` previously added
`[&_img]:w-full [&_img]:h-full` per-item to fight the same bug on
desktop. That menu item no longer renders an `<img>`, so the override
was already dead code; removed here for hygiene.
**Safety check on the wider change:** every other menu item that
contains a nested `<img>` already sets its own sizes explicitly:
- `ProxySettingsSub` (`MobileMenu.tsx`): `<ExternalImage className="w-4
h-4" />`
- `WorkspacePill.tsx`: `[&_svg]:!size-3.5 [&_img]:!size-3.5` on the menu
content
Direct-child icons (`ChevronRightIcon`, `CircleHelpIcon`, `XIcon`,
lucide icons in `UserDropdownContent.tsx`) remain direct children of the
menu item, so `[&>svg]:size-icon-sm` keeps sizing them as before.
</details>
---
_Authored by Coder Agent on behalf of @tracyjohnsonux._
This allows a Coder Agents chat to detach from its linked workspace
without deleting or changing the workspace, so a different workspace can
be linked later. It adds detach controls wherever the linked workspace
appears, including the workspace pill menu, fallback workspace badges,
and the workspace picker. The workspace selection state now updates
consistently across desktop and mobile.
Running workspace:
<img width="453" height="296" alt="image"
src="https://github.com/user-attachments/assets/ac5197a7-f0f4-4123-bbea-d3ddaca7a3e4"
/>
Stopped workspace:
<img width="389" height="203" alt="image"
src="https://github.com/user-attachments/assets/f5a8a90c-4bb0-405a-ade3-791146687b2d"
/>
Closes CODAGT-510