## Summary
Move chat model management and runtime availability to
organization-scoped API routes. The API now identifies the organization
in every model-management operation, applies chat model RBAC and ACLs
within that organization, and returns only models the caller can use
there.
Depends on #27958.
## Problem
Chat model configurations now belong to organizations, but the existing
experimental management and discovery APIs are deployment-shaped. Their
routes do not identify an organization, SDK methods do not accept an
organization ID, and item operations address a model configuration by ID
alone.
That contract cannot safely expose organization-owned model
configurations. It also does not provide the organization-specific
provider information, authorization behavior, ACL management, or
recovery guidance needed by the settings and chat clients later in the
stack.
## Fix
Add organization-scoped chat model endpoints under:
```text
/api/experimental/organizations/{organization}/chats/models
```
The new API provides:
- Organization-scoped model listing and creation.
- Model get, update, and delete operations scoped by both organization
and model ID.
- Model ACL read and update operations.
- Organization-scoped runtime availability and provider status.
- Redacted provider descriptors for model-management clients.
- Chat model RBAC and row ACL enforcement for writes, runtime discovery,
and Chatd model selection.
- Recovery guidance when a chat's organization has no usable local
model.
Historical chats can retain a model reference owned by another
organization, but runtime selection uses an authorized local default
when one is available. Foreign, inaccessible, disabled, and otherwise
unusable models are not exposed as usable selections for the
organization.
The existing collection and availability routes remain as
default-organization compatibility endpoints in this layer:
```text
GET /api/experimental/chats/model-configs
POST /api/experimental/chats/model-configs
GET /api/experimental/chats/models
```
These compatibility routes do not provide access to models in
non-default organizations. The previous unscoped item update and delete
routes are removed because item operations must include the owning
organization.
## Breaking change
This changes the experimental HTTP API and Go SDK contracts for chat
model management.
Clients must migrate model-management and availability requests to the
organization-scoped routes and provide an organization identifier. In
the Go SDK, the following methods now require an organization ID:
- `ChatModelAvailability`
- `ChatModels`
- `CreateChatModel`
- `UpdateChatModel`
- `DeleteChatModel`
`ChatModels` also changes its return value from `[]ChatModel` to
`OrganizationChatModelsResponse`, which includes the organization models
and the redacted provider descriptors required by management clients.
New item and ACL methods likewise require both the organization ID and
model ID.
The default-organization compatibility routes reduce the immediate
impact for collection and availability callers, but they do not preserve
the previous item API or SDK method signatures. Generated clients and
direct consumers of the experimental API must be updated before adopting
this change.
_This pull request description was generated by Coder Agents._
Rename the admin-managed SDK resource from `ChatModelConfig` to
`ChatModel`. Rename catalog entries from `ChatModel` to
`ChatModelCatalogEntry` and replace `ChatModelsResponse` with
`ChatModelAvailabilityResponse`.
API routes and JSON fields do not change. Experimental SDK and OpenAPI
schema names do change.
Depends on #27957
_This pull request description was generated by Coder Agents._
Add the `chat_model_config` RBAC resource, actions, and API key scopes.
This layer includes the scope migration and generated API types. Model
list queries apply token scopes, SQL authorization filters, and row
ACLs.
Write and default-management handlers keep deployment configuration
authorization until the next API layer replaces it.
Depends on #27956
_This pull request description was generated by Coder Agents._
Include the owning organization ID for each chat model configuration in
telemetry snapshots.
Depends on #27955
_This pull request description was generated by Coder Agents._
## Summary
Scope chat model configurations and runtime model resolution to
organizations. Each model configuration now belongs to exactly one
organization, each organization can have its own default model, and
Chatd only uses models that belong to the chat's organization.
Depends on #27968.
## Problem
Chat model configurations are currently deployment-wide even though
chats belong to organizations. A default model, explicitly selected
model, or configured model override can therefore reference the same
model configuration from every organization. This prevents model
catalogs and defaults from being isolated by organization and allows a
chat to retain or select a model that its organization does not own.
Existing model configurations also have no organization ownership or
organization-scoped ACL data, and the database enforces a single live
default across the entire deployment.
## Fix
Add required organization ownership and ACL storage to chat model
configurations. The migration assigns every existing configuration to
the default organization, seeds organization-wide read access, and
replaces the deployment-wide default constraint with a per-organization
constraint.
Scope Chatd lookup, admission, fallback, and queue-promotion behavior to
the chat's organization:
- New chats use the usable default model from their organization.
- Explicit model selections from another organization are rejected.
- Deployment or personal overrides that point to a foreign model are not
used for the chat.
- Historical cross-organization model references remain unchanged in the
database, but runtime generation falls back to a usable local default.
- Queue promotion resolves unavailable, disabled, missing, or foreign
stored models to the usable local default and fails when no usable local
default exists.
The existing model configuration management and availability routes
remain compatibility routes for the default organization in this layer.
A later layer in the stack adds organization-scoped management routes
and updates clients to use them.
## Breaking change
This changes chat model behavior from deployment-wide to
organization-scoped. Existing configurations are migrated only to the
default organization; they are not copied into every organization.
As a result:
- A non-default organization no longer inherits the deployment's
existing default model.
- Creating a chat in an organization without a usable local default
fails with `400: No default chat model config is configured.`
- Explicitly selecting a configuration owned by another organization
fails with `400: Model config does not belong to the specified
organization.`
- Existing chats whose stored model belongs to another organization can
generate only if a usable local default is available; otherwise
generation fails.
Deployments using Agents in non-default organizations must configure a
usable default chat model separately for each organization after the
organization-scoped management layers in this stack are available.
_This pull request description was generated by Coder Agents._
Chat model updates could merge PATCH fields from a row read before the
serialized transaction. Deletes could also succeed after another request
deleted the row.
Merge updates from the row read after the write lock. Make soft deletes
report missing rows, and validate provider availability and model
compatibility in the same transaction.
This PR also removes `DeleteChatModelConfigsByAIProviderID`. The query
has no production callers, including the AI provider deletion path, and
only remained in generated database wrappers and mocks. Its removal is
intentional dead-code cleanup and does not change provider deletion
behavior.
_This pull request description was generated by Coder Agents._
Updates the Coder Agents license card to consistently refer to
concurrent agents, removes the usage management link, and links to the
licensing and usage documentation.
Updates Storybook coverage for the revised labels, tooltip, and link
behavior.
<details>
<summary>Coder Agents disclosure</summary>
This pull request was generated by Coder Agents on behalf of @jaaydenh.
</details>
Closes
[CODAGT-632](https://linear.app/codercom/issue/CODAGT-632/gray-out-the-archive-chat-button-when-chat-is-running).
The chatd state machine only permits archiving a chat family from idle
execution states (`waiting`/`error`); archiving an active chat fails
with HTTP 409. The agents UI still offered "Archive agent" and "Archive
& delete workspace" for active chats, and `AgentsPageLayout` showed a
confirm dialog claiming "Archiving it will interrupt the current run",
which is no longer true: confirming just surfaced the 409 error toast.
## Changes
- `ChatActionsMenuItems` gains a `chatFamilyAllowsArchive` helper
mirroring the backend rule: archive is allowed only when the root chat
and every embedded child are `waiting`/`error` (archive cascades
atomically over the family, so an active child also rejects it). Unknown
status stays fail-open with the server 409 as backstop. A new
`isArchiveBlocked` prop disables both archive items; unarchive, pin, and
rename are unaffected.
- While blocked, the menu shows a visible hint ("Interrupt or wait for
the agent to finish first.") and both disabled items reference it via
`aria-describedby`.
- Sidebar rows (`ChatTreeNode`, covering the dropdown and the
right-click context menu) gate on the chat list record, which watch
events already keep fresh. The chat top bar gates on the live stream
status (`liveChatStatus`) plus the same list record's children, passed
through the layout outlet context, so all three menus read one live
source and re-evaluate without a reload.
- Removed the stale "Archive agent?" interrupt-confirm dialog and its
pending-state plumbing from `AgentsPageLayout`; archive now mutates
directly.
- "Archive & delete workspace" is now archive-first: the archive call is
the atomic server-side eligibility check and completes before the
destructive delete, so a chat that becomes active mid-flow can never
lose its workspace. On delete failure the chat deliberately stays
archived, with no compensating unarchive: the delete outcome can be
ambiguous client-side (a late 5xx can arrive after the delete build
committed), so restoring the chat could resurrect it while its workspace
is being deleted. Instead the mutation refetches all chat collections on
any failure and the toast directs the user to the archived filter, where
Unarchive is one click. This is a deliberate departure from the
self-restoring retry surface of #26900. The workspace-deletion confirm
dialog is unchanged.
## Scope
The client gate is best-effort UX; the server-side 409 is the
correctness backstop. Rare staleness (a chat outside the loaded sidebar
pages, events missed across a socket reconnect) leaves the actions
enabled and the server rejects the request with the existing error
toast. Cache-coherency hardening for those windows was deliberately
removed to keep the change proportionate to the defect.
## Testing
- Storybook play stories: disabled archive items with the accessible
hint for an active chat and for an idle parent with a running child;
enabled items for an idle chat; in the top bar menu and the sidebar
dropdown/context menu (`ChatTopBar.stories.tsx`,
`ChatsSidebar.stories.tsx`).
- Unit tests: archive-first ordering, delete failures keep the chat
archived and rethrow (`agentWorkspaceUtils.test.ts`).
- `pnpm -C site check`, `pnpm -C site lint:types`, whole-file vitest
runs for every touched test/story file.
- Remote dogfood UAT on a deployment built from this branch: archive
items disabled while running and in `requires_action`, re-enabled live
on transition to `waiting`, archive works from `waiting` and `error`
with no confirm dialog, unarchive unaffected.
> Shux acted on Mike's behalf to create this pull request.
## Summary
In the Template Builder base template select grid, each base template
card's "official" checkmark badge was rendering below the card title
(`h3`) instead of inline and vertically centered with it.
## Root cause
Tailwind preflight was enabled in #27821 (removed `corePlugins: {
preflight: false }`), which applies `svg { display: block }`. The badge
relied on `align-middle`, which is a no-op on block-level elements, so
the icon dropped onto its own line. The same badge in `ModuleCard` was
patched in #27821 with `inline-block`, but the identical badge in
`TemplateCard` was missed.
## Fix
- `TemplateCard.tsx`: add `inline-block` to the badge so it flows inline
within the title's text (trailing the last word and wrapping naturally),
with `align-middle` handling vertical centering. This matches the
existing `ModuleCard` pattern.
- `Response.tsx`: update a stale comment that referenced preflight being
disabled.
## Verification
- `biome check` passes on both files.
- Confirmed via a repo-wide sweep that no other inline icon layouts
regressed from the preflight change (all other icon-next-to-text usages
are protected by flex/inline-flex containers, `asChild` class merging,
or are sole children).
<img width="1083" height="626" alt="Screenshot 2026-08-21 at 12 02
47 PM"
src="https://github.com/user-attachments/assets/2183dbf4-1f4f-4aae-92cf-21c11efebc5e"
/>
<details>
<summary>Investigation notes</summary>
Timeline:
- Preflight was explicitly disabled via `corePlugins: { preflight: false
}`.
- The official badge (#26806, #27036, #27797) worked because the lucide
SVG rendered inline while preflight was off.
- #27821 ("refactor: remove MUI and Emotion") removed the
`corePlugins.preflight: false` block, enabling preflight and its `svg {
display: block }` rule. It patched `ModuleCard` with `inline-block` but
missed `TemplateCard`.
A read-only sweep across `site/src` found no additional broken
inline-icon layouts. Candidates like `DERPRegionPage` (icon inside an
`inline-flex` `Link`) and `TemplateCustomizationsStep`
`BaseTemplateCard` (intended vertical layout) are safe.
</details>
---
_This PR was generated by Coder Agents on behalf of @jeremyruppel._
Points the "Start an unlimited trial" link in the concurrency-limit
queued callout (`QueuedForCapacityCallout`) to the internal
`/deployment/premium` route instead of the external
`https://coder.com/trial` URL, matching the convention used elsewhere in
the app (e.g. `LicensesSettingsPageView`, `Paywall`).
Uses `Link asChild` + `RouterLink` so navigation is client-side (SPA)
instead of a full page reload.
Also fixes all four links in the callout (`Start an unlimited trial`,
both `learn more` links, and the `sales@coder.com` mailto) rendering at
12px (`size="sm"`) while the surrounding message text is 14px. Dropping
the explicit `size="sm"` lets the links fall back to `Link`'s default
size, matching `AlertDescription`'s `text-sm` (14px).
<details>
<summary>Decision log</summary>
- Confirmed `/deployment/premium` is an existing internal route
(`PREMIUM_PAGE_PATH` constant in `Paywall.tsx`), not an external link.
- Existing pattern for internal links elsewhere in the codebase is
`<Link asChild showExternalIcon={false}><RouterLink to="..." /></Link>`,
so this change follows that convention rather than using a plain `<a
href>`.
- Only the admin-on-Community-license path (`canManageLicenses &&
!hasLicense`) is affected by the route change; the member and
paid-license admin messages are unchanged.
- Verified in a local `develop.sh` dev instance: HMR picked up the
change cleanly, TypeScript reports 0 errors, and the links now render at
14px matching the message body.
</details>
---
PR generated with Coder Agents.
**TLDR:** When someone using free Coder taps a "Start trial for free"
button, we now write down which page they tapped it on, and whether they
went on to actually start the trial. That tells us which locked features
make people want Premium.
## Brief summary
- Records two things: the button being clicked, and a trial actually
being started.
- Each click gets a random ID that travels with the person to the trial
form, so we can tell who finished from who walked away.
- Records a short label for the page (like `appearance` or `audit_log`),
never the web address, so no customer names are collected.
#28226 added the trial form and has since merged, so this now targets
`main`.
## Description
Today the "Start trial for free" button just moves you to the Premium
page and nothing is recorded, so there is no way to know which feature
convinced someone.
This adds two events:
| Event | Happens when |
|---|---|
| `cta_click` | Someone clicks "Start trial for free" |
| `trial_signup` | A trial licence is actually issued |
The click generates a random ID. It rides along to the trial form and
gets saved on the signup, so a click and a signup can be matched up
later. Clicks with no matching signup are the people who gave up. Trials
started without a paywall (for example straight from the sidebar) are
labelled `direct`, so "no label" never gets confused with "broken".
A few small choices worth knowing:
- The ID is kept in browser session storage, not in the web address, so
the URL stays clean. It expires after 30 minutes.
- The trial signup event is recorded by the server, not the browser, so
nobody can fake a signup.
- The browser cannot say which kind of event it is sending. It can only
report a click; the server labels it.
- Only people allowed to start a trial are counted. The server rejects
the rest, which matters because a few paywalls show their button to
everyone.
- Nothing is recorded at all when telemetry is turned off.
There is deliberately no "paywall was shown" event. Every admin page
visit would report one, which is a lot of noise for little insight.
Every paywall in the [Premium Copy
Updates](https://www.notion.so/coderhq/Premium-Copy-Updates-3c1d579be59280a59557cabd54fd8a1f)
table has a label. The rows that do not are badges and alerts with no
button to click.
## Proof
Ran a local Coder with no licence and clicked the paywall button on the
Appearance page.
What the browser sent, and the reply:
```json
POST /api/v2/deployment/premium-funnel-events -> 204 No Content
{"id":"92d96afa-f975-4789-b8a7-b1fd19d4df85","source":"appearance","variant":"premium"}
```
What the browser saved for the trial form, same ID:
```json
{"id":"92d96afa-f975-4789-b8a7-b1fd19d4df85","source":"appearance","createdAt":1787268520857}
```
The click then landed on the Premium page with the trial form.
Screenshots of the network panel, the saved value, and the Premium page
are attached below. The form itself was not submitted, since that would
contact the real licence server.
## Testing
- Backend tests: a click is recorded with the server's own label, an
unknown page label is refused, and a normal member is refused.
- Storybook tests: button clicked and ID saved on all three paywall
styles, plus the case where the viewer only sees guidance text and has
nothing to click.
- Unit tests for the saved ID: expiry, bad data, unknown page label, and
the `direct` fallback.
- `make gen` is clean, and lint, types, and formatting pass.
Two stories fail on `main` today and fail the same way here:
`ExternalAuthSettingsPageView > Page` and `PremiumPageView > No
License`. Neither file is touched by this branch; both were verified
failing on `main` with this branch checked out elsewhere.
## Follow-up
coder/coder-telemetry-server#45 saves these events for reporting. It
merges after this one.
<details>
<summary>Plan and decisions</summary>
**Goal:** find out which Premium paywall drives trial signups, and tell
abandons apart from completions.
**Options considered and dropped:**
1. Put `?from=appearance` in the URL. Rejected: messy for users.
2. Use router state. Rejected: lost if the page is refreshed.
3. Let the browser report the signup. Rejected: it could be faked or
lost during the redirect.
4. Work out the page from the URL. Rejected: URLs contain organization
and template names.
5. Report every paywall impression. Rejected: one event per admin page
visit is noise, and the click is the signal.
6. Separate event types and tables. Rejected: makes the matching query
harder for no gain.
**Event shape:**
```go
type PremiumFunnelEvent struct {
ID uuid.UUID // event ID; for a click this is also the matching ID
EventType string // cta_click | trial_signup, set by the server
Source string // appearance, audit_log, ..., direct
Variant string // which paywall design was shown
AttributionID uuid.UUID // the click this signup came from
UserID uuid.UUID
CreatedAt time.Time
}
```
**Where the code lives:** nothing under `components/` imports from
`modules/`, so the paywall components stay presentational and only gain
an `onCTAClick` prop. New wrappers in `modules/paywall/` own the
reporting, one per paywall style, each taking a fixed `source`.
**Self-review:** the `frontend-review` skill caught that reading
permissions inside the wrapper forced an auth context into
presentational page views, which broke their stories. Every call site
already passes `canViewPremium={permissions.viewAllLicenses}` and the
button only renders when that is true, so the wrapper no longer reads
permissions and the server stays the gate. It also flagged one
unattributed paywall on the Security page, now wired up as
`browser_only`.
</details>
> Generated with [Coder Agents](https://coder.com/agents) on behalf of
@david-fraley
The Storybook suites fail on main in two independent ways: three
interaction tests in the locally-run Vitest suite (`pnpm vitest run
--project=storybook`, which does not run in CI), and three story
captures in CI's pixel snapshot job, whose failures do not fail the
GitHub check (the job PATCHes `status=failed` to pixel.coder.com and
still exits 0).
Vitest interaction failures:
* `ModelForm > Cost Estimate Shows Loading While Debounce Pending` has
been broken since it landed in #28298. In add mode the model identifier
field is the Known Model autocomplete, which only commits typed text to
`form.values.model` on option selection or dropdown close, so typing
free text left the form's model empty and the pricing section never
entered its loading state. The story now selects the catalog option.
This also fixes the same story's capture failure in the pixel job.
* The `SecretsPageView` import error stories raced the dialog enter
animation: `findByText` resolves on DOM presence, and under
parallel-suite CPU load `toBeVisible()` can run while the Radix dialog
content is still at `opacity: 0` from its `fade-in-0` animation. These
now poll with `waitFor(...toBeVisible())`, matching the file's other
import stories.
Pixel capture failures:
* `NavbarView > ForMCPCreateOnlyAdmin` / `ForMCPDeleteOnlyAdmin` inherit
the meta's tablet+desktop matrix but click the "Admin settings" button
that only exists in the desktop navbar, so their tablet captures always
fail. They now pin `pixelWithDesktop` like every other admin story in
the file.
* `AgentsPageLayout > OrganizationScopedMCPServers` fails intermittently
because `CompactOrgSelector` option names are timing-dependent: when an
org icon fails to load (as in the pixel static server), the avatar
fallback initials render and join the option's accessible name (`"MY My
Organization 2"`), breaking exact-name queries and polluting
screen-reader output. The decorative avatar is now `aria-hidden`, which
was verified red-green by forcing unloadable icons locally.
> [!NOTE]
> Shux (AI agent) authored this PR on behalf of @ibetitsmike.
<!-- shux-attribution: model=claude-opus-4-6 thinking=high -->
Phase 2 of the Scalable Approach for Adding New IDE Session Types RFC,
tracked in #27410.
Adds a session_counts map to Stats on the Agent API, so an agent can report
a count per app name instead of the four fixed fields, which are now marked
deprecated. Bumps the API to v2.11.
- coderd normalizes names on ingest, caps distinct entries, and sums the
overflow under unknown, so an arbitrary client name cannot blow up
cardinality.
- Agents predating v2.11 set the fixed fields instead, never both, and the
ingest path falls back to them.
Nothing sends session_counts yet. The agent starts populating it in #28338.
Follow-up to #28392. The `DateTimeRangePicker` popover opened with
`align="start"` (left-aligned to the trigger), while the other dropdowns
on the page use `align="end"`. This changes the `PopoverContent`
alignment to `align="end"` for consistency.
One-line change; no behavior or layout changes inside the dropdown
itself.
---
Generated by Coder Agents on behalf of @tracyjohnsonux.
Phase 1 of the Scalable Approach for Adding New IDE Session Types RFC,
tracked in #27410.
Gives agent session accounting one canonical vocabulary for app names, so
later work can report a count per app without the agent and the server
disagreeing about what an app is called.
- Adds codersdk/appname.go: NormalizeAppName for storage and lookup, and
AppFamilyName for the places that need a bounded value, such as a
Prometheus label or the Connection_Type enum.
- agentssh.Server keeps session counters in a map keyed by normalized name
instead of three fixed atomics, dropping entries at zero.
- Drops MagicSessionType. Sessions carry plain app names, and unrecognized
ones flow through as themselves rather than collapsing to unknown.
Wire behavior is unchanged: the agent still reports the four session_count_*
fields over Agent API 2.10.
Settings pages rendered the premium upsell banner at different vertical
offsets and the IdP sync pages used a lighter heading style than the
rest of the settings UI.
Three causes:
1. The IdP Sync (`/organizations/:org/idp-sync`) and Organization IdP
Sync (`/deployment/idp-org-sync`) pages hand-rolled their headers with a
bare `<h1 className="text-3xl m-0">` (missing `font-semibold`) and a
`flex flex-col gap-12` wrapper (48px above the banner).
2. Workspace Proxies and Appearance wrapped `SettingsHeader` in flex
columns with their own `gap` (`gap-4`, `gap-8`), which stacked on top of
the header's built-in `pb-6`, pushing the banner to 40px and 56px
offsets respectively while Groups sits at 24px.
3. `SettingsHeaderDescription` used `leading-normal` (21px lines) while
the inline `Link` component ("View docs") renders a 24px-tall
`inline-flex leading-6` box, inflating the line box on pages whose
description contains a docs link and shifting content below by ~3px.
Changes:
- Replace the custom header markup on both IdP sync pages with the
shared `SettingsHeader`, `SettingsHeaderTitle`, and
`SettingsHeaderDescription` components; the Export Policy button moves
into the `actions` slot.
- Hoist `SettingsHeader` out of the gap wrappers on Workspace Proxies
and Appearance so the header's `pb-6` is the single source of
header-to-content spacing; the gap now applies only within the entitled
content.
- Set `SettingsHeaderDescription` to `leading-6` so description line
height matches the inline `Link`, whether or not a docs link is present.
All affected pages now start content, including the premium paywall,
24px below the header description.
_Created by Coder Agents on behalf of @tracyjohnsonux._
Follow-up to #28392. Wires the new `DateTimeRangePicker` into the AI
Gateway sessions page, replacing the text-expression
`DateTimeRangeFilter`, and removes the redundant "Filters" preset
button.
- The picker trigger sits in the filter row alongside the search field
and the User/Provider/Client/Model dropdowns.
- The preset queries (All sessions, My sessions) behind the Filters
button duplicate the search field and the user dropdown, so the button
is dropped by passing no presets. Other pages using the shared `Filter`
component are unaffected.
- No API or URL contract changes: the filter query still stores resolved
`started_after`/`started_before` timestamps. The picker's preset id is
display-only component state, shown only while the URL range still
matches what the preset resolved to, so shared links keep rendering as
custom ranges.
- Clearing the search resets the picker to the default window, labeled
"Last 24 hours".
- The picker trigger chevron now uses the shared animated
`ChevronDownIcon`, rotating on open like the other dropdown triggers.
- The old `DateTimeRangeFilter` component is left in place (still
referenced by its stories); removing it can be a separate cleanup.
Storybook: the sessions filter story now asserts the Filters button is
gone and exercises opening the picker; existing stories updated to the
new value shape. `biome`, `tsc`, and the Storybook interaction tests for
the page pass.
---
Generated by Coder Agents on behalf of @tracyjohnsonux.
`TestUserSecretAuditDiffRedaction` flakes when create and update audit
rows receive equal timestamps and `GetAuditLogsOffset` returns the tied
rows in either order.
Identify audit rows by action, or by diff content when both actions are
writes, instead of relying on slice position. Fixescoder/internal#1625.
Reviewed and updated by Coder Agents on behalf of @dylanhuff-at-coder.
Adds a frontend-only `DateTimeRangePicker` component: a date range
picker with quick picks (Last 15 min, Last hour, Today, This week) and
per-boundary time selection. The dropdown opens as just the quick-pick
list; the calendar and From/To time fields only appear after selecting
"Custom range". Quick picks commit immediately, custom ranges commit via
Apply.
No backend changes. The value is always resolved dates:
```ts
interface DateTimeRangeValue {
start: Date;
end: Date;
preset?: string; // display metadata only, never sent to the backend
}
```
Quick picks resolve to concrete `start`/`end` at selection time;
`preset` exists solely so the trigger can keep rendering the preset
label. Consumers send `start`/`end` to the API in UTC via
`Date.toISOString()`.
Composed from existing primitives (`Calendar`, `Popover`, `Select`,
`Input`, `Button`); the `Calendar` component is unchanged. Includes 9
Storybook stories with play-function coverage: quick-picks-only open
state, immediate preset commit, custom panel expansion, apply flow,
invalid time validation, end-before-start validation, and cancel
discarding the draft.
<details>
<summary>Design notes and follow-ups</summary>
## Mockup deviations
- Added an Apply/Cancel footer to the custom panel. The design mockup
does not show one, but a draft range needs an explicit commit point.
- Trigger uses the shared `Button` outline variant to match the design
system.
## Settled decisions
- The API receives resolved timestamps, not preset tokens.
- All values sent to the backend are UTC; the component works in
browser-local time and conversion happens at the API boundary.
## Follow-ups (separate PRs)
- Wire the component into the pages that need it, with query-param
persistence where ranges should be shareable.
- The existing `DateTimeRangeFilter` (text-expression based, used by the
AIBridge sessions filter) is a candidate for migration to this design.
- Possible polish per design feedback: masked/segmented time input,
optional seconds, 24-hour locale support.
</details>
---
Generated by Coder Agents on behalf of @tracyjohnsonux.
The empty `MultiSelectCombobox` rendered ~46px tall next to the fixed
40px `Input`, breaking form rows that place them side by side, most
visibly the IdP sync mapping fields (`/organizations/{org}/idp-sync`,
IdP org sync, template/parameter forms).
## Root cause
#27821 removed MUI's `CssBaseline` and enabled Tailwind preflight
(`corePlugins.preflight` was previously `false`). Preflight sets `input
{ line-height: inherit }`, so the combobox's inner cmdk input started
inheriting the wrapper's `text-sm` 24px line instead of the browser
default. With its `py-2.5` padding the empty combobox computed to 24 +
20 + 2 = 46px, while `Input` stays pinned at `h-10`.
This is a sibling of the `ModelSelector` regressions fixed in #28399,
but in the shared combobox primitive, so that fix could not reach these
pages.
## Fix
- Pin the inner cmdk input to `leading-5` and reduce the empty-state
padding to `py-2` (20 + 16 = 36px row).
- Make the wrapper a `flex flex-col justify-center` so the row centers
within `min-h-10`, giving exactly 40px when empty while the selected
state still grows with its badges.
## Validation
- Red-green verified with a temporary geometry probe story: combobox
wrapper 46px before, 40px after (Input 40px in both). The probe was not
committed (FE10) and no new story ships with this PR per review.
- `pnpm test:storybook` for `MultiSelectCombobox`, `IdpSyncPageView`,
`IdpOrgSyncPageView`, and `DynamicParameter`: all pass.
- `biome check` and `tsc -p .` clean.
> 🤖 This PR was written by Coder Agents on behalf of @tracyjohnsonux.
Centralizes every LLM client build in chatd behind one resolver and
makes all model calls behave the same, instead of preserving per-flow
quirks.
## Why
`chatd` built LLM clients in five places (standard turn, compaction
override, quickgen, title override, advisor), each repeating config
parsing, route resolution, client construction, and debug wrapping with
slightly different behavior. An earlier iteration of this PR centralized
construction but preserved every historical quirk behind
purpose-specific spec constructors and policy flags. Review concluded
the quirks themselves were the problem.
## What changed
- `Server.resolveModelCall` (`modelcall.go`) is the single pipeline from
a `modelCallSpec` to a ready client plus call metadata. Callers build
spec literals inline; the only spec fields are data: model source (chat
config, explicit config row, or fixed provider/model pair for computer
use), requested reasoning effort, chatd-scoped routing, and build
options.
- Uniform behavior for every call:
- Provider options are always derived. Whole-chat summaries, turn status
labels, and chat-model compaction summaries previously sent none; they
now follow the config like everything else.
- `MaxOutputTokens` always defaults to 32k when the config leaves it
unset. The compaction summary call nulls the cap after copying the
template because it is non-streaming and the Anthropic SDK rejects
non-streaming requests whose cap implies a completion over ten minutes.
- Debug recording always wraps the client when chat debug is enabled,
labeled with the resolved provider/model. The debug-rebuild paths
(`manualTitleDebugSpec`, `quickgenDebugSpec`) are deleted; debug helpers
now only create and finalize run records, since run association flows
through the generate context, not the model.
- Clients are always constructed with the configured model string and
unmutated route, so AI Gateway validation sees the name exactly as
configured. The resolved identity feeds metadata, logs, and debug labels
only.
- `resolvedModelCall.newCall` / `newObjectCall` are the only production
constructors of `fantasy.Call` / `fantasy.ObjectCall`; object calls now
carry the config's sampling parameters.
- Net: -240 lines.
## Validation
- Full `./coderd/x/chatd/...` suite green (PostgreSQL-backed).
- Red-green toggles verified for the three key unifications (option
derivation, token default, summary cap removal).
- ARCHITECTURE.md: prose the refactor made stale is deleted; no new
resolver documentation is added.
> Mux acted on Mike's behalf for this PR.
When the last token of a follow-up prompt is an absolute filesystem path
(e.g. `check /var/log/syslog`), the skill slash menu opened with "No
skills match that query." and swallowed Enter, so the prompt could not
be submitted without adding a trailing space or quoting the path.
Follow-up composers always pass slash commands, so the menu opened for
any `/`-token; the Enter handler then consumed the keypress
unconditionally while the menu was open.
Three fixes (CODAGT-956):
- Enter with no selectable match now dismisses the menu and lets the
same keypress fall through to the submit handler. While a skill source
is still loading (personal skills fetching, or workspace skills
unknown), a zero-match Enter stays consumed so a pending fetch cannot
let a would-be skill trigger submit as raw text; a no-match Tab still
falls through so keyboard users can leave the composer.
- The menu is anchored to the composer box instead of a caret-derived
rect, so it no longer follows the cursor while the query is typed.
- The menu renders directly above the chat input at exactly the
composer's width on desktop, matching its pinned-above-composer
presentation on small viewports. The caret-rect plumbing (live caret
measurement, fixed 1px anchor span, scroll/resize listeners) is removed.
Storybook interaction coverage was updated accordingly:
`FilteredEmptyEnterClosesAndSubmits` (replaces
`FilteredEmptyKeepsMenuOpen`), `TrailingPathEnterSubmits`,
`EnterWhileSkillsLoadingDoesNotSubmit`, `MenuStaysAnchoredWhileTyping`,
and `OpensAboveAnchorAtAnchorWidth`. Each new story was verified red
against the reverted fix and green with it. Remote dogfood UAT validated
the Enter/anchoring/placement scenarios and regressions in the real UI
across two rounds (round 2 covering the composer-width presentation).
Fixes
https://linear.app/codercom/issue/CODAGT-956/slash-menu-improvements
> 🤖 This pull request was created by Mux on behalf of @ibetitsmike.
<!-- mux-attribution: model=claude-opus-4-6 thinking=high -->
On small viewports, the agent chat page showed the wrong panel. The
right panel filled the screen and hid the chat.
before:
https://github.com/user-attachments/assets/fa3986b8-949a-4aad-8e98-d64316ade3f5
after:
https://github.com/user-attachments/assets/bc56359d-2ffe-4f95-b47b-638261379da8
## Changes
- Below the `lg` breakpoint, the page shows one panel at a time. A right
panel left open on a wide window used to hide the chat when the window
narrowed. The page now shows the chat.
- The panel preference is not changed by a resize. When the window
widens, the panel comes back. When the user taps a tab while narrow, the
panel opens as before.
## Tests
- Unit tests for the new `useRightPanelNarrowSuppression` hook cover
mount, both breakpoint crossings, and the explicit override.
- A new mobile-viewport story asserts the chat is visible when a panel
was left open.
<details>
<summary>Decision log</summary>
- The right panel's open state persists in localStorage across all
viewport sizes. `max-lg:hidden` on the chat made that stale state hide
the chat on narrow windows.
- The narrow suppression is transient state, not a write to the
persisted preference. A temporary resize keeps the user's desktop layout
intact.
- The hook adjusts state during render (the React "adjusting state when
a prop changes" pattern) instead of an effect, per FE8.
- Verified all resize sequences live against a dev deployment: narrow
with a panel open, widen to restore, drill in while narrow, and explicit
close while narrow.
</details>
🤖 This PR was created by [Coder
Agents](https://coder.com/docs/ai-coder/agents) on behalf of @bpmct.
---------
Co-authored-by: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com>
Third in the stack, split out of #27873. This is where dynamic client
registration first produces a public client.
- A registration requesting token_endpoint_auth_method `none` issues no
secret and persists the client_type derived in #28043. The response
omits client_secret entirely, per RFC 7591 §3.2.1.
- Discovery still advertises only the two secret-based methods. `none`
is withheld until the token endpoint honors it, so a conforming client
is not told to attempt an exchange that would be rejected.
- The app and its secret are written in one transaction. They were two
independent inserts, so a failed second one left a committed app that
could never authenticate while still holding a registration access
token. Pre-existing, but a public client's missing secret row makes the
orphaned case unspottable by inspection.
- registration_client_uri uses url.JoinPath, fixing a trailing-slash
access URL producing //oauth2/clients/{id}.
- Docs state both limits above, so the page does not describe a flow
that returns 400 until #28047 lands.
A client registered here cannot obtain a token yet; #28047 adds that.
Dynamic client registration is off by default, so none of this is
user-visible until then.
Refs
https://linear.app/codercom/issue/ENG-3029/oauth2-support-public-client
Refactors the mantle SigV4 signing middleware into a new shared
`intercept/bedrocksig` package.
Prepares for OpenAI protocol support on the mantle provider (follow-up
PR).
---
Generated by Coder Agents on behalf of @johnstcn.
## What
Two small docs fixes, split out of the offlinedocs Fumadocs migration
(#27390) so that PR stays a focused tooling change and these land on
their own review track.
- `docs/about/contributing/documentation.md`: the Vale style-guide link
pointed at `docs/.style/style-guide/`, which is excluded from the
published docs corpus, so the relative link 404s on the live site.
Repointed to an absolute github.com URL.
- `docs/ai-coder/ai-gateway/monitoring.md`: lifted the "Possible client
values" `<details>` block out of the list item so it renders as a
top-level accordion, and restored a dropped blockquote continuation
marker (`>`).
## Why
Both are genuine docs bugs on their own:
- The `.style/` link is broken on the live site today, because that
directory is intentionally excluded from the published corpus.
- The `<details>` nested inside a list item renders inconsistently;
lifting it to a sibling block is portable across renderers.
They are also prerequisites for the offlinedocs Fumadocs migration,
whose stricter build-time sync hard-fails on unmapped inter-doc links
and only converts a `<details>` to an accordion when it is a list
sibling. Landing them here first lets the migration PR drop these two
files on its next rebase.
## Validation
- `make fmt/markdown` and `make lint/markdown`: clean (via
pre-commit-light).
- Docs-only change; no CI or build config touched.
> This PR was created with AI assistance (Coder Agents).
## Summary
Streamlines the agent instruction files, cutting per-session token
overhead by roughly 65%.
The core design change: `AGENTS.md` no longer `@`-imports WORKFLOWS.md,
package.json, the eight detailed guides, or `AGENTS.local.md` into every
session. Guidance moves from guaranteed-in-context to load-on-demand
through a routing table, betting that one-line guardrail summaries plus
links are enough to make agents pull the right doc when a task needs it.
- `AGENTS.md` (251 -> 80 lines): a routing table to the canonical docs
in `.claude/docs/`, the essential command table, and one-line guardrails
for the architectural footguns (SQLC workflow plus audit table, OAuth2
error conformance, chatd architecture-doc rule, Swagger annotations,
`InTx` discipline, test concurrency, workflow `permissions: {}`,
mandatory git hooks, commit/PR title format).
- `site/AGENTS.md` (364 -> 89 lines): keeps the FE1-FE10 contract
summary, TypeScript LSP navigation, pnpm command table, React Compiler
boundaries, accessibility rules, and the pre-PR `frontend-review`
requirement. Drops the generic React tutorial sections that restate
framework documentation.
- The persona and relationship prose ("Our relationship", foundational
rules) is deliberately removed. The behavioral gates worth keeping for
every agent survive as one-liners: get explicit permission before
excepting a rule, discuss architectural decisions before implementing,
and answer questions instead of jumping to implementation.
- Unique operational content is relocated rather than deleted:
Playwright failure-artifact paths and the CI `playwright-artifacts-`
naming convention move to `.claude/docs/AGENT_FAILURES.md`; the
`React.useId()` rule, the `key={String(booleanState)}` remount footgun,
date-as-prop Storybook determinism, and the suppression-comment rule
(updated from `eslint-disable` to `biome-ignore` to match current
tooling) move to `.claude/docs/FRONTEND_PATTERNS.md`.
Content reflects current main: the chatd rule says agents leave TODOs
and the human PR author writes the architecture doc, and MUI/Emotion are
described as removed, not deprecated.
Note for Claude Code users: `CLAUDE.md` symlinks to `AGENTS.md`, so
detailed workflow docs (hook internals, the new-feature checklist,
`AGENTS.local.md`) are no longer auto-loaded into every session; agents
must follow the links.
## Validation
- `pnpm exec markdownlint-cli2` on all four touched files: 0 errors
- `pnpm exec markdown-table-formatter --check` on all four touched
files: pass
- `make lint/emdash`: pass
- `make lint/agents`: pass
- All relative links verified to resolve
> Mux created this PR and description on behalf of Mike.
Since #28300, `process_output` tool rows in the Agents chat transcript
fed the result payload's `running: true` flag into the row's active
state. That flag is a frozen snapshot of whether the polled process was
still alive, so for long-lived background processes (a dev server that
never exits) every completed poll kept its shimmer label and spinner
animating forever, even after the agent went idle.
`ToolCall.Root` now receives the actual tool-call status, so the shimmer
and spinner stop when the poll completes. The `running: true` snapshot
still controls what it was meant to: label tense ("Checking npm start"
while alive vs "Checked npm start" after exit) and kill-signal badge
visibility. The existing `ProcessOutputStillRunningResult` story now
also asserts the running spinner is absent on a completed poll (verified
red-green: reintroducing the bug fails exactly that story), and the
intent story asserts the spinner is present while the tool call is
genuinely running.
> [!NOTE]
> Mux (AI agent) authored this PR on behalf of @ibetitsmike.
<!-- mux-attribution: model=claude-opus-4-6 thinking=high -->
Follow-up to the review discussion on the price book refresh automation:
https://github.com/coder/coder/pull/28146#discussion_r3806439966
## Problem
The price seed row struct was duplicated three times, so a JSON tag
change had to be repeated in each copy:
- `scripts/aibridgepricesgen.priceRow`
- `scripts/aibridgepricesdiff.priceRow`
- `coderd/aibridge/prices.seedRow`
The `(provider, model)` key and its parsing were duplicated too.
## Change
Add `coderd/aibridge/prices/pricebook` holding the shared contract:
`Row`, `Key`, `Row.Key()`, and `Parse`. The package depends only on the
standard library, so the scripts can import it without pulling in the
database layer that `coderd/aibridge/prices` brings.
Consumer-specific behaviour stays where it is: upstream conversion and
validation in the generator, comparison and Markdown rendering in the
diff tool, DB seeding and runtime lookup in the seeder.
The batch SQL upsert still extracts JSON fields by name, so it remains a
separate contract boundary. The note about keeping it in sync now lives
on the shared `Row`.
No generated artifacts change; the serialized schema is identical.
---
<sub>Opened by Coder Agents on behalf of @evgeniy-scherbina.</sub>
Selecting a model on form-style pages (deployment AI settings such as
`/ai/settings/coder-agents`, the personal agent settings override rows,
and the advisor override) rendered a broken trigger: the provider icon
pinned to the far-left edge, the model name floating in the center, and
the chevron on the right. At desktop widths the field also collapsed to
content height (~22px) next to the 40px Clear/Save buttons.
Two defects in the shared `ModelSelector` trigger:
1. #28330 added the provider icon as a third direct flex child of the
trigger button. Form call sites override the trigger with
`justify-between` plus a width wider than its content, which spread
icon, label, and chevron apart. The icon and label now form a single
flex child so they stay adjacent under any `justify-*` override; the
chevron stays a direct child because the `[&>svg]` trigger styles target
it, and the label still truncates via `min-w-0`.
2. The base classes carried composer-pill responsive variants
(`md:h-auto`, `md:w-auto`, `md:shrink-0`). tailwind-merge resolves
conflicts per modifier, so `md:h-auto` silently defeated the call sites'
`h-10` at the `md` breakpoint (and `md:w-auto` defeated `w-[22rem]` on
the advisor row). The pill's responsive sizing now lives at its call
site, so plain `h-10`/width overrides win everywhere. The composer pill
is visually unchanged.
Also fixes the provider icon shrinking next to long model names: the
icon's `span` wrapper was a plain flex item whose automatic minimum size
collapses (preflight's `img { max-width: 100% }` scales the icon down as
the wrapper shrinks). The wrapper is now a `shrink-0` flex container, so
the label truncates while the icon keeps its 12px size (verified
red-green with a temporary probe: 7.3px without the fix).
Adds `FormFieldTriggerKeepsIconAndLabelAdjacent` and
`FormFieldTriggerTruncatesLongModelName` stories reproducing the
form-style trigger for visual snapshot coverage. Both defects were
verified red-green during development with temporary geometry probes
(icon-label gap 63.5px and height 30px without the fixes; gap 4px and
height 40px with them); per FE10 the committed story carries no geometry
assertions and the pixel snapshot guards the layout.
Also recolors the advisor "Uses / turn" and "Max tokens" field labels
from `content-placeholder` to `content-secondary` on the same settings
surface.
Remote dogfood UAT ran against this branch across desktop and narrow
viewports and passed.
> 🤖 Mux acted on behalf of @ibetitsmike to create this PR.
Since #28326 made the row actions trigger (kebab) persistently visible
on the selected chat, it overlapped the archiving spinner: both render
right-aligned in the same 28px box (the kebab is `absolute inset-0` over
the timestamp/spinner slot), so archiving the selected chat produced a
garbled spinner-plus-dots glyph. The overlap could previously only occur
transiently on hover; the persistent kebab made it guaranteed on the
active row.
Render the kebab only when the row is not showing the archiving spinner,
restoring the intent of the existing `ArchivingShowsSpinnerOnly` story
(which only covers a non-selected row, so it never caught this).
<details>
<summary>Diagnosis and validation</summary>
**Diagnosis**
- The only right-side loader in a sidebar row is the `Spinner` rendered
by `ChatTreeNode` when `isArchiving && archivingChatId === chat.id`.
- #28326 added `isActiveChat && "opacity-100"` to the kebab trigger,
which shares the same `w-7` box as the spinner. The spinner branch never
suppressed the trigger, only the timestamp span had the hover/active
`hidden` classes.
- Result: on the selected chat, `EllipsisVerticalIcon` and the 8-leaf
`Spinner` render on top of each other for the entire archive mutation.
**Fix**
- `ChatTreeNode.tsx`: gate the `DropdownMenu` on `!isArchivingThisChat`
so the spinner and trigger are mutually exclusive. Row context-menu
actions remain available and are already disabled while archiving.
**Validation**
- `pnpm test:storybook
src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx`:
53 passed
- `make pre-commit`: pass (lint/ts, lint/go, gen, fmt, build)
</details>
_Generated by Coder Agents on behalf of @tracyjohnsonux._
## Summary
Subtle visual tweak to the agents sidebar chat list. Chat titles remain
`text-content-primary`, but titles for chats that are not currently
selected now
render at 85% opacity. The selected chat keeps full opacity (and its
existing
`font-medium` weight), making the active chat stand out more clearly.
## Changes
- `ChatTreeNode.tsx`: apply `opacity-85` to the title span when the chat
is not
active, keeping `font-medium` for the active chat.
`opacity-85` is a valid Tailwind 3.4 utility and is already used
elsewhere in
the codebase.
---
_This PR was generated by Coder Agents on behalf of @tracyjohnsonux._
The docs named the `ai_model_price:update` permission but never said
which role holds it, and nothing anywhere explained that a price is
keyed by provider type and model, so every configured provider of that
type shares it. This names the Owner role, documents the keying and why
`openai-compat` cannot be priced, and repeats both in the CLI's
`--provider` descriptions and `update` help.
> [!NOTE]
> Initially generated by Claude Opus 5, modified and reviewed by
@ssncferreira
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
Refreshes the six stale screenshots on the
[Workspaces](https://coder.com/docs/user-guides/workspace-management)
page (`docs/user-guides/workspace-management.md`) with the current Coder
UI, recreating equivalent dummy data (matching template and workspace
names) so the images stay recognizable.
Linear: DOCS-55
## Updated images
| Image | Section |
|-------|---------|
| `creating-workspace-ui.png` | Creating workspaces |
| `workspace-update.png` | Updating workspaces |
| `workspace-automatic-updates.png` | Automatic updates |
| `user-guides/workspace-bulk-actions.png` | Bulk operations |
| `schedule.png` | Starting and stopping |
| `templates/template-variables.png` | Repairing workspaces |
Prose is unchanged; this is a screenshot-only refresh.
## Notes / limitations
The originals came from production-like deployments with
AWS/Azure/Kubernetes templates, multiple users, and aged data. These
were reproduced on a local `scripts/develop.sh` (Docker) instance, so:
- Template/workspace names, states (Running/Stopped/Failed), the
"Selected 3 of 5" bulk selection,
`use_kubeconfig=True`/`namespace=default`, and the auto-update =
`Always` setting all match.
- Instance-derived values cannot match: "used by" counts show `0
developers`, build times show `Unknown`, the connection-latency pill has
no value, and the workspace name field is disabled by the template.
<details>
<summary>How these were produced (decision log)</summary>
Followed the `dogfood` skill:
1. Started a local instance with `./scripts/develop.sh`
(`v2.36.0-devel`, matching the docs version).
2. Created Docker-backed templates named to match the originals:
`data-warehouse`, `analytics`, `api-services`, `frontend-web`,
`pod-share-jupyter`, `aws-linux-ephemeral`, `azure-aks-pod`, plus
`kubernetes2` ("Develop in Kubernetes"). The JetBrains module was
dropped from the Docker template to avoid a plan-time error;
`kubernetes2` uses a Coder-only variant declaring the real `namespace`
and `use_kubeconfig` variables so it imports with `use_kubeconfig=true`.
3. Created workspaces: `admin/my-frontend` (outdated → update available,
with a schedule), `admin/dev` (auto-update = Always), and
`kirby/{data-science, aws-large, azure-space, ephem-demo (Failed), kirbs
(Stopped)}`.
4. Captured each page with a headless Playwright script, hiding dev-only
chrome (the sticky deployment banner, the navbar `::before` hazard
stripe, and the `-devel` version chip) and cropping settings pages to
the form. Images are 2x for crispness.
5. Each screenshot was visually verified against the original for
content parity.
</details>
Split out of #28045
([PLAT-479](https://linear.app/codercom/issue/PLAT-479)) so the coverage
rules can be reviewed on their own. First of three. No behavior change:
both functions are added without production callers, and the OAuth2
authorize endpoint consumes them in the next PR.
**ScopesCover(allowed, requested)** reports whether every permission the
request grants is also granted by the allowlist. It expands both sides
and compares permissions rather than names, so coder:workspaces.access
covers workspace:read without ever naming it.
The fail-closed rules are the part worth reviewing:
- Coverage models site-level grants only. A scope carrying org or user
permissions, a negative permission, or a narrowed allow list is refused
on either side rather than compared on the part that is modeled.
Answering "covered" from the fraction that was read would report
authority that was never examined.
- An unknown name on either side is an error, not a false, since a
caller cannot tell "not covered" apart from "could not decide".
- A wildcard request is covered only by a wildcard grant. Enumerating
every workspace action that exists today does not cover workspace:*,
because the wildcard also authorizes the actions added tomorrow.
**CanonicalScopeName** maps the aliases IsExternalScope accepts, all and
application_connect, onto the api_key_scope enum spellings.
IsExternalScope answers whether a name may be requested, not how it is
spelled once persisted, so anything storing a validated name has to
canonicalize in between. Both sides of ScopesCover must already be
canonical, which its parameter names restate at every call site.
Covered by 17 coverage cases, guard cases that drive each unmodeled
shape through both sides of the comparison, and an exhaustive sweep over
the external catalog: coder:all covers all of it, and every catalog name
covers itself.
Stack: this PR, then negotiate and persist the scope, then the consent
page and invalid_scope redirect.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Right-clicking the kebab (⋮) button on a chat in the agents sidebar was
showing two menus: the kebab's own dropdown **and** the row-level
right-click context menu.
The kebab lives inside the row's \`ContextMenuTrigger\`, so the
\`contextmenu\` event on the button bubbles up and opens the row's
context menu on top of the dropdown that the button already owns.
Fix: swallow \`contextmenu\` on the kebab \`Button\` so only its
dropdown is associated with the button. Right-clicking anywhere else on
the row still opens the row context menu as before.
> Generated with Coder Agents on behalf of @tracyjohnsonux.
---------
Co-authored-by: Coder Agents <noreply@coder.com>
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
Resolves
[DEVEX-721](https://linear.app/codercom/issue/DEVEX-721/the-logs-selector-per-startup-script-needs-to-scroll-on-overflow).
Cap the overflow menu height and let it scroll, matching the existing
pattern already used by `DownloadSelectedAgentLogsButton` (`max-h-56
overflow-y-auto`). No component swap needed, the `DropdownMenu` was the
right primitive, it just lacked height/scroll constraints.
```diff
-<DropdownMenuContent align="end">
+<DropdownMenuContent align="end" className="max-h-56 overflow-y-auto">
```
<img width="504" height="325" alt="image"
src="https://github.com/user-attachments/assets/363d9bc0-e3c3-4355-8d05-abb8c650a37a"
/>
<details>
<summary>Investigation / decision log</summary>
- The "logs selector per startup script" is the overflow kebab menu in
`site/src/modules/resources/AgentRow.tsx`. When per-script log tabs
don't fit horizontally, `useKebabMenu` moves the extras into a Radix
`DropdownMenu` rendered as `DropdownMenuRadioItem`s.
- `menuContentClass` (`site/src/components/DropdownMenu/menuClasses.ts`)
is `z-50 min-w-48 overflow-hidden ...` with no max-height, so an
overlong menu is clipped instead of scrolling.
- `DownloadSelectedAgentLogsButton.tsx` already solves the identical
problem on its own menu with `className="max-h-56 overflow-y-auto"`, so
we reused that for consistency.
- Considered switching to the `Select` component (which has
`SelectScrollUpButton`/`SelectScrollDownButton` chevrons) but that's a
larger re-model of the trigger + items and reconciliation with the
`Tabs`/`useKebabMenu` wiring. Deferred in favor of the minimal fix.
</details>
Closes
[DES-22067](https://linear.app/codercom/issue/DES-22067/update-and-restart-action-uses-a-cloud-icon).
The "Update and restart" / "Update and start" actions used `CloudIcon`,
which reads as a cloud/network concept instead of an update-and-restart
build action.
Switch them to `RotateCcwIcon`, which the UI already uses for:
- the `Restart` button on the workspace page
- the `Update` action in `WorkspaceOutdatedTooltip`,
`AgentOutdatedTooltip`, and `SubAgentOutdatedTooltip`
Call sites changed:
- `WorkspacePage/WorkspaceActions/Buttons.tsx` - `UpdateButton`
- `WorkspacesPage/WorkspacesTable.tsx` - "Update and start workspace"
and "Update and restart workspace" primary actions
- `WorkspacesPage/WorkspacesPageView.tsx` - batch "Update" menu item
The `requireActiveVersion` variant still uses `PlayIcon`, since that
path starts the workspace on the active version rather than offering a
choice. Worth a separate design pass if that should change too.
<details>
<summary>Investigation notes</summary>
Verified against `origin/main` (`af90d8e2be`): no prior PR referenced
DES-22067 and all three `CloudIcon` usages were still present. `grep`
confirms no `CloudIcon` usages remain in `site/src` after this change.
Icon conventions found in the codebase:
- `RotateCcwIcon`: restart, update (outdated tooltips), retry
(RetryButton, TasksTable, TaskPage, HealthLayout)
- `RefreshCwIcon` / `RefreshCcwIcon`: softer "reload data" flows
(terminal, secrets, prebuilds invalidate, inbox, git panel)
`data-testid="bulk-action-update"` is preserved on the batch update
item, so e2e coverage is unaffected.
</details>
---
Generated by Coder Agents on behalf of @designertyler.
Playwright's `--with-deps` flag performs a live apt refresh and install
in the E2E and Storybook jobs, even though the runner images already
contain the required system libraries.
Install only Playwright's pinned Chromium build in CI so apt mirror
failures cannot block browser setup.
Closes DEVEX-792