Commit Graph
5658 Commits
Author SHA1 Message Date
Susana Ferreira 3deecb481e chore: remove ai-gateway-cost-control experiment flag (#27579)
## Description

Closes
[AIGOV-443](https://linear.app/codercom/issue/AIGOV-443/remove-ai-gateway-cost-control-experiment-flag-once-feature-is-stable).

The AI Gateway cost control feature is planned for GA on the upcoming
release, so this removes the `ExperimentAIGatewayCostControl` experiment
and all of its gating. The cost control API endpoints remain gated by
the `FeatureAIBridge` license feature (the AI Governance add-on), so
this only drops the experiment layer.

## Changes

- **`codersdk/deployment.go`**: remove the
`ExperimentAIGatewayCostControl` const, its `DisplayName()` case, and
its `ExperimentsKnown` entry.
- **`enterprise/coderd/coderd.go`**: remove the
`httpmw.RequireExperiment(...)` gating from the AI cost control routes.
They keep `RequireFeatureMW(codersdk.FeatureAIBridge)`. Affected
endpoints:
  - `GET /organizations/{organization}/groups/ai/spend`
- `GET
/organizations/{organization}/groups/{groupName}/members/ai/spend`
  - `GET /organizations/{organization}/ai/spend/export`
  - `GET /groups/{group}/members/ai/spend`
  - `GET /groups/{group}/ai/spend`
- `GET/PUT/DELETE /users/{user}/ai/budget/override` and `GET
/users/{user}/ai/spend`
- **`enterprise/coderd/aibridge_test.go`**: drop the experiment from
test setup and remove the now-obsolete `RequiresExperiment`
negative-path tests.
- **Frontend (`site/src/...`)**: remove the `ai-gateway-cost-control`
experiment checks from the cost control UI (Groups pages, user dropdown)
and their stories/mocks. The feature is now driven solely by the
`aibridge` feature visibility.
- **Generated**: regenerated `coderd/apidoc/*`,
`docs/reference/api/schemas.md`, and `site/src/api/typesGenerated.ts`.

## Out of scope

The dogfood `CODER_EXPERIMENTS` config lives in a separate infra repo,
not `coder/coder`. Leaving `ai-gateway-cost-control` there is harmless:
unknown experiment values are logged as `"ignoring unknown experiment"`
at startup and otherwise ignored, so no ordering dependency or breakage.
That cleanup can be a follow-up.

<details>
<summary>Implementation notes</summary>

- Verified how unknown experiments are handled in `coderd/coderd.go`
`ReadExperiments`: unknown values produce a warning log and are inert,
so removing the definition before the dogfood config is updated is safe.
- Noticed the group `ai/budget` routes (`/groups/{group}/ai/budget`)
were already gated only by `FeatureAIBridge`, never by the experiment.
After this change all cost control routes are uniformly feature-gated,
resolving that inconsistency.
- Removed an obsolete `RequiresExperiment` subtest in
`TestUserAISpendStatus` that only asserted a 403 from the experiment
gate; with the gate gone it would no longer be blocked pre-RBAC.

</details>

---

_This PR was created by Coder Agents on behalf of @ssncferreira._
2026-07-29 14:59:58 +01:00
Susana Ferreira d6a5c8e9f8 refactor: make user AI budget and spend endpoints consistent (#27611)
## Description

Makes the user AI cost control endpoints consistent.

## Changes

- Replaces the flat `spend_limit_micros` and `limit_source` fields on
`GET /users/{user}/ai/spend` with a nested `effective_budget`, reusing
the type behind `group_budget`. The flat pair made it possible to encode
a limit without a source.
- Renames `AIGroupBudget` to `AIBudgetLimit`, since it also carries
`user_override` limits and is no longer group-specific. The type name is
not part of the wire format.
- Moves `/users/{user}/ai/budget` to `/users/{user}/ai/budget/override`.
The endpoint only ever managed the per-user override, which the type,
the handlers, and the operation IDs all already said; the path was the
only place that didn't.

> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by
@ssncferreira
2026-07-29 14:01:32 +01:00
Susana Ferreira e71249a821 fix: ai cost control cap configurable AI spend limit (#27640)
## Problem

A configured AI spend limit was only validated as `gte=0`, with no upper
bound. The group spend query multiplies the per-member limit by the
number of attributed members, so a large enough limit overflows `bigint`
and fails the whole query, returning an error for every group in the
request rather than just the misconfigured one.

## Changes

- Add `MaxAISpendLimitMicros`, $1,000,000 per member per budget period.
- Reject group budgets and per-user overrides above the maximum with a
400 naming the limit.
- Bound both budget forms in the UI so they show the valid range before
submitting.

Follow-up
https://github.com/coder/coder/pull/27589#discussion_r3668956350
Depends on https://github.com/coder/coder/pull/27589

> [!NOTE]
> Initially generated by Claude Opus 5, modified and reviewed by
@ssncferreira
2026-07-29 14:00:13 +01:00
Michael Suchacz fb30674806 fix(site/src/pages/AgentsPage): order chat transcript by message id (#27620)
## Context

Follow-up to #27495 (append-order guarantee for `chat_messages.id`) and
#27619 (prompt query ordering), both merged. This PR applies the same id
ordering to the transcript the user actually sees.

## Why?

`buildOrderedMessageIDs` in `chatStore.ts` sorted by `created_at`, which
is `now()` and therefore shared by every row in an insert batch. It
builds `orderedMessageIDs`, which is what the transcript renders, so it
re-imposed the ordering the backend PRs remove.

The failure needs the merge path, not a plain fetch. Initial REST
hydration already sorts by numeric id before reaching the store, and
`Array.prototype.sort` is stable, so a single correctly ordered response
rendered correctly. But `upsertDurableMessages` copies the existing
message `Map`, appends new ids, and re-sorts. `Map` iteration is
insertion ordered, so when a refetch or reconnect merges earlier ids
into a map that already holds later ones, the stable timestamp sort
faithfully preserves the wrong order.

This also makes the store consistent with `useChatStore.ts` and
`api/queries/chatMessageEdits.ts`, which already sort by `id`.

## Changes

`buildOrderedMessageIDs` now calls `toSorted` with an `id` comparator
inlined at its single call site, and the `byMessageCreatedAt` helper is
gone. `ChatMessage.id` is a `number` in `typesGenerated.ts`, backed by a
Go `int64`, so numeric subtraction is correct.

## Testing

Two vitest cases, both verified red by restoring the timestamp
comparator:

- `sorts messages by id when created_at disagrees with append order`
returned `[2,1]`.
- `orders merged messages by id rather than by arrival` returned
`[3,4,1,2]`, the exact inversion the merge path produces.

`MergedMessagesRenderInIDOrder` in `ChatPageContent.stories.tsx` covers
the same merge path through the rendered timeline.

All 334 `ChatConversation` unit tests and the 4 `ChatPageContent`
storybook interaction tests pass, and `tsc -p .` plus biome are clean.

> Opened by Mux on behalf of Mike.
2026-07-29 12:43:11 +00:00
Michael Suchacz c17bed25e0 feat: wire chat lifecycle hooks into chatd (#27429)
Wires chat lifecycle hooks into chatd, gated by the
`agent-lifecycle-hooks` experiment. Part of the lifecycle hooks stack
(#27401, #27428, #27430). See `docs/admin/setup/chat-lifecycle-hooks.md`
for the consumer-facing contract.

## Summary

When a hook URL is configured, chatd dispatches `session_start`,
`user_prompt_submit`, `pre_tool_use`, `post_tool_use`, `pre_compact`,
`post_compact`, and `stop` events to the consumer and applies its
responses.

## Design

- **Stateless**: Coder stores no hook dispatch or decision state.
Delivery is at least once; consumers deduplicate on stable payload
identifiers (chat ID, event type, tool-use ID) and answer duplicates
with the same decision.
- **Admission-time prompt effects**: `user_prompt_submit` dispatches
exactly once per submission (create, send, queue, edit, subagent spawn)
and folds its effects into the stored prompt as typed message parts:
original-or-overridden user parts, then model-only `hook-context`, then
a user-visible `hook-notice`. Hook context is stripped from every
client-facing conversion; hook notices are excluded from model prompts.
The server rejects hook parts in client-submitted content.
- **Tool gating**: `pre_tool_use` allow can override tool input; deny
becomes a synthetic denied tool result, with any returned model context
persisted as a model-only transcript row so it never reaches clients.
The denial text identifies an external policy (the deployment's
lifecycle hook) as the source and marks the decision as persistent, so
the model explains the denial instead of retrying it or misreporting it
as an infrastructure failure.
- **Fail closed**: a dispatch failure rejects the triggering request or
moves the chat to the error state in the same transaction as the
affected step, so a runnable state is never published with unapproved
content.
- **Admission before persistence**: `pre_tool_use` is dispatched for the
calls the model produced, before the assistant message is stored. See
"Staged tool admission" below.
- **Fresh dispatch per tool call**: every non-provider-executed tool
call is decided by its own `pre_tool_use` dispatch; Coder never reuses
an earlier decision on the consumer's behalf. Retries re-dispatch the
same logical event.

## Structure

All hook dispatch flows through one seam: entry points build a
`chathooks.Chat` (chat identity) and a `chathooks.Message` (event
details) and call `Trigger.Trigger`, the only component that talks to
the dispatcher. The integration lives in the `coderd/x/chatd/chathooks`
subpackage, split by responsibility:

- `trigger.go`: the trigger seam; builds the wire envelope per event,
normalizes deny into a typed error, and holds the package's single
enabled-check.
- `effects.go`: pure conversion of hook results into transcript rows and
prompt parts.
- `errors.go`: failure classification (dispatch error messages, denial
mapping, tool-result dispatch-failure scanning).
- `tooluse.go`: the tool-call gate (`pre_tool_use` preflight,
`post_tool_use` payloads, applying admitted input to the step).

Server-bound glue stays in `coderd/x/chatd/hook_server.go`: the
chat-parking dispatch error handlers, the step-commit row insertion
wrappers, and the dynamic post-tool-use state loader, which depends on
chatd validation types.

This PR adopts the `codersdk/x/agenthooks` and
`coderd/x/agenthooks/dispatch` import paths introduced at the tip of
#27401; intermediate commits still reference the pre-move paths and are
not individually buildable.

## Staged tool admission

`pre_tool_use` originally ran at tool execution time, which is after the
assistant message carrying the tool call was already committed. An
`input_override` therefore had to rewrite stored message content in
place. @hugodutka pointed out that chatd treats message content as
immutable, and that the rewrite was a shortcut rather than a
requirement.

It was also a correctness problem in its own right: the rewrite only
updated the database, so the transcript could show one input while a
different one had executed.

The hook now runs before the step is persisted:

```text
provider stream ends (tool calls complete, in memory)
  -> pre_tool_use dispatch per call
  -> ONE transaction: assistant row with admitted inputs, synthetic denials, hook rows
  -> execute
```

The step is inserted once, carrying the input the tool runs with.
`UpdateChatMessageContentByID` and `Tx.UpdateMessageContent` are deleted
from #27428, so message content stays immutable.

Two consequences, both intentional:

- **Clients converge rather than wait.** Tool-call parts still stream
live, so a rewritten call briefly shows the model's proposed input
before the committed message replaces it. The chat store already clears
stream state when an assistant message arrives, so the stored input wins
with no frontend change and no added latency before tool cards appear.
- **A call already in history was already admitted.** Execution consumes
the stored input instead of dispatching a second decision, which keeps
one dispatch and one set of hook effects per call. A consumer policy
change between admission and execution applies to later calls, not to
calls already admitted.

The per-chat debug endpoint still records the provider's original tool
input. Its purpose is to report provider behavior, and it requires an
explicit per-chat debug flag; the invariant here covers the transcript.

## Configuration

Adds `chat-hook-url`, `chat-hook-secret`, `chat-hook-timeout`, and
`chat-hook-enabled` deployment options with startup validation. The
flags are hidden from `coder server --help` while the feature is
experimental; the setup guide documents them.

## Tool input validation

Built-in tool arguments reach a consumer as raw JSON with key spelling
preserved, but the tools decode those bytes with Go, which matches
struct fields case-insensitively and keeps the last match. A policy
reading `path` could therefore authorize one value while the tool
executed another, and a lone case variant such as `{"PATH":"/secret"}`
was invisible to a policy checking for `path`.

Coder now rejects a built-in tool call whose input repeats a key or
spells a schema property with different capitalization, before the
`pre_tool_use` dispatch, so a consumer is never asked to authorize bytes
whose meaning depends on the reader. Rejected calls produce an error
result the model can retry; unambiguous calls in the same batch still
run. A consumer-authored `input_override` is rechecked after the
dispatch and fails the turn closed, because the model cannot correct it.
Dynamic and MCP inputs are excluded because the client and the workspace
agent execute those calls rather than coderd.

Two paths needed more than a schema check. Execution resolves a
deprecated tool name to its canonical tool, so validation resolves
aliases first. The `edit_files` decoder also reads `search` and
`replace`, which its schema does not advertise, so those aliases are now
matched exactly and their case variants ignored.

A hook denial now returns a structured 403 carrying `kind:
"hook_denied"`, mirroring the dispatch-failure response that already
carries its own kind. Without it a client cannot tell a policy decision
apart from a generic failure, and the chat UI titled a denial "Request
failed". Adding a kind needs no migration: `ChatErrorKind` is persisted
only inside the JSONB `chats.last_error` column, whose decoder accepts
unknown kinds.

The hook docs also correct the tool-input convergence window. A batch
dispatches sequentially before the assistant row commits, so the
original input stays visible for a span that scales with the number of
tool calls in the step rather than a single hook timeout.

> This PR was written by Mux, an AI coding agent, on Mike's behalf.
2026-07-29 11:39:12 +00:00
Susana Ferreira 0b4095085e fix: report combined member limit in group AI spend (#27589)
## Problem

The organization groups page showed each group's AI budget as the
group's per-member limit, so the total it displayed was effectively
group members × group budget. That ignores per-user budget overrides
charged to the group, so a group where one member has an override
reported a limit that doesn't match what its members can actually spend.

## Changes

- Add `total_spend_limit_micros` to the organization groups AI spend
payload, the combined budget of the members attributed to the group,
with each member's override replacing their share.
- Return `null` for the total when the group has no budget, since its
members spend without a cap.
- Both the organization groups and single group spend endpoints report
the new field, as they share the same query.
- Use the total as the denominator on the groups page AI budget column.

Depends on #27568
2026-07-29 09:16:37 +01:00
Jaayden Halko 06ceb4253d feat: add agent runtime hour license claims and entitlement feature (#27459)
Licenses can now carry three agent runtime hour claims:
`agent_runtime_hours_allocation`, `agent_runtime_hours_limit_soft`, and
`agent_runtime_hours_limit_hard` (unit: hours). They surface as the new
usage-period feature `agent_runtime_hours` in `GET
/api/v2/entitlements`, where `limit` carries the allocation and the new
optional `soft_limit` / `hard_limit` fields on `codersdk.Feature` carry
the thresholds.

Invalid combinations reject the entire license via `validateClaims`
(both at upload and when computing entitlements for stored licenses):
soft/hard without allocation, negative allocation, soft outside `0 <=
soft < allocation`, or `hard < allocation`.

Soft and hard limits are not comparison inputs in `Feature.Compare`;
they ride along with whichever license wins (newest `iat`, existing
behavior). None of the three claim names is a feature name, so old
servers ignore them via the existing unknown-claim tolerance, protecting
rollout of licenses minted with the new claims.

The claim name constants defined in `enterprise/coderd/license` are the
canonical contract for `github.com/coder/license` (X1).

Part of
[CODAGT-837](https://linear.app/codercom/issue/CODAGT-837/a1-agent-runtime-license-claims-and-entitlement-feature).
Blocks B4 (usage wiring + warnings), C1 (hard-limit admission gate), F1
(licenses page), A4 (managed-agent coexistence), X1 (licensor).

Out of scope, handled by follow-up issues: `Actual` usage wiring,
threshold warnings, admission gating, premium defaults, and FE surfacing
beyond regenerated types.

<details>
<summary>Implementation plan and decision log</summary>

## Decisions (confirmed by jaayden, 2026-07-23)

1. **Claim names / unit:**
   - `agent_runtime_hours_allocation` - allocation (unit: hours, int64)
   - `agent_runtime_hours_limit_soft` - soft limit
   - `agent_runtime_hours_limit_hard` - hard limit
- None of the three claim names is itself a `FeatureName`; all three map
to the single new usage-period feature `agent_runtime_hours`
(`FeatureAgentRuntimeHours`), mirroring how `managed_agent_limit_soft`
mapped onto `managed_agent_limit`. Old servers therefore ignore all
three claims via the `FeatureNamesMap` check.
2. **Reject-license.** Invalid claim combinations reject the whole
license via `validateClaims` (upload returns 400 via
`ParseClaimsIgnoreNbf`; already-stored licenses produce an `Invalid
license ... parsing claims` entitlements error and contribute nothing).

## Design notes

- `codersdk.Feature` had a `SoftLimit` field until 051ed34580 ("feat:
convert soft_limit to limit", #22048) collapsed managed-agent soft/hard
into a single `limit`. This reintroduces soft/hard as optional fields
without changing managed-agent behavior.
- Existing usage-period machinery populates `UsagePeriod` from
`nbf`/`exp` (`usagePeriodStart`/`usagePeriodEnd` in
`LicensesEntitlements`); reused unchanged, consistent with managed
agents.
- `Entitlements.AddFeature` replaces whole `Feature` structs (no
merging), so soft/hard automatically ride along with the winning
license. No `Feature.Compare` logic change; doc updates plus tests pin
that soft/hard are not comparison inputs.
- The feature name itself is not accepted as a claim; the allocation
must come from the dedicated claim so it is validated against soft/hard
(prevents a validation bypass where a direct feature-name claim could
win precedence with unvalidated thresholds).
- The generic "enabled but not entitled/expired" warning loop skips the
feature, mirroring `FeatureManagedAgentLimit`; usage-based warnings
arrive with B4.
- No premium default for this feature (unlike managed agents).

## Changes

1. `codersdk/deployment.go`: new `FeatureAgentRuntimeHours` (in
`FeatureNames`, `UsesLimit()`, `UsesUsagePeriod()`, keeping it out of
`FeatureSet` expansion); `Feature.SoftLimit`/`Feature.HardLimit`
(`soft_limit`/`hard_limit`, omitempty); doc updates for `UsagePeriod`
and `Compare`.
2. `enterprise/coderd/license/license.go`: canonical claim constants;
validation helper called from `validateClaims`; al-la-carte loop maps
the allocation claim to the feature and attaches soft/hard from the
companion claims; skips for the companion claims and the raw feature
name; generic warning loop skip.
3. `enterprise/coderd/coderdenttest`: `AgentRuntimeHours(allocation)`
builder.
4. Tests:
- `TestAgentRuntimeHoursLicenses`: entitled/grace round-trips (including
JSON field assertions), allocation-only, explicit zero,
`IssuedAtRanking` mirror, soft/hard ride-along with a newer
allocation-only license, direct feature-name claim ignored,
unknown-claims compatibility (old-server simulation).
- `TestAgentRuntimeHoursClaimValidation`: table of valid/invalid claim
combinations against `ParseClaims`, plus stored-license entitlements
error.
- `TestPostLicense`: API-level 400 rejection and a happy-path POST +
`GET /api/v2/entitlements` round-trip.
- `TestFeatureComparison`: soft/hard ignored in comparison; newest `iat`
wins over larger soft/hard.
5. `make gen`: regenerated `site/src/api/typesGenerated.ts`,
`coderd/apidoc/*`, `docs/reference/api/*`.

## Verification

- `go test ./enterprise/coderd/license/ ./codersdk/` and `go test
./enterprise/coderd/ -run 'TestPostLicense|TestEntitlements'` pass.
- `golangci-lint` clean on changed packages; `make lint/emdash` clean;
FE `tsc --noEmit` clean.
- Independent agent review of the diff found no blockers; its minor
findings (direct feature-name claim validation bypass, precedence test
gap, missing API happy-path test) were addressed.

</details>

> [!NOTE]
> Generated by Coder Agents on behalf of @jaaydenh (Linear CODAGT-837
agent session).
2026-07-29 07:58:30 +01:00
Jake Howell d072aa7bd0 feat(site): confirm before batch stopping workspaces (#27631)
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.

## What

Bulk stopping workspaces from the workspaces table currently fires
immediately with no confirmation, whereas the single-row **Stop** action
and the bulk **Delete** / **Update** actions all show a dialog first.
This adds a confirmation dialog to the bulk **Stop** action so it
matches the rest.

## How

- Added `BatchStopConfirmation`, a small `ConfirmDialog` wrapper
mirroring the wording of the single-workspace stop confirmation but
pluralized for the selected count.
- Wired it into `WorkspacesPage`: `onBatchStopTransition` now opens the
dialog (`setActiveBatchAction("stop")`) instead of calling
`batchActions.stop(...)` directly, and the actual stop runs on confirm.
Added `"stop"` to the `BatchAction` union.

No change to the underlying `batchActions.stop` behavior (still only
stops `running` workspaces).

<details>
<summary>Reviewer notes</summary>

Before: `onBatchStopTransition={() =>
batchActions.stop(checkedWorkspaces)}` — no confirmation.

After: opens `BatchStopConfirmation`; confirm calls
`batchActions.stop(checkedWorkspaces)` then clears the active action,
consistent with how `BatchDeleteConfirmation` and `BatchUpdateModalForm`
are handled.

</details>

---
_Opened as a draft. Disclosure: this PR was generated by Coder Agents on
behalf of @jakehwll._
2026-07-29 04:26:09 +00:00
Bobby Ho fbac602456 feat!: add admin-controlled dynamic client registration toggle (#27316)
`POST /oauth2/register` (RFC 7591 Dynamic Client Registration) has
exactly one gate today: `ExperimentOAuth2`, a static, process-lifetime
flag that wraps the entire `/oauth2/*` route tree as an all-or-nothing
switch. That flag is scheduled for removal at GA, which would leave DCR
with zero admin control at all once it is gone.

Add a persistent, DCR-specific `oauth2_dcr_enabled` deployment setting,
independent of the experiment system, so admin control over DCR survives
GA. `POST /oauth2/register` checks the flag and rejects new
registrations with an RFC 7591-shaped `403` when disabled; discovery
metadata (`GET /.well-known/oauth-authorization-server`) conditionally
omits `registration_endpoint`. A new audited `GET`/`PUT
/api/v2/oauth2-provider/settings` endpoint lets an owner toggle it live,
no restart required. The setting defaults to disabled, matching the
canonical design proposal; disabling only stops new self-registrations,
clients that already registered continue to authorize and exchange
tokens normally.

Address issue described in
[ENG-3056](https://linear.app/codercom/issue/ENG-3056/oauth2-dcr-admin-configurable-enabledisable).

## Where this sits in the request path

```mermaid
sequenceDiagram
    autonumber
    participant A as Admin
    participant S as coderd
    participant DB as site_configs<br/>(oauth2_dcr_enabled)
    participant C as OAuth2/MCP Client

    Note over A,S: Admin toggles DCR (new)
    A->>S: PUT /api/v2/oauth2-provider/settings<br/>{dynamic_client_registration_enabled: false}
    S->>S: authorizeContext(ActionUpdate, ResourceDeploymentConfig)
    S->>DB: UPSERT oauth2_dcr_enabled = false
    S-->>A: 200 OK (audited)

    Note over C,S: Client discovery + registration afterward
    C->>S: GET /.well-known/oauth-authorization-server
    S->>DB: GetOAuth2DCREnabled (system ctx, every request, no cache)
    DB-->>S: false
    S-->>C: 200 metadata, registration_endpoint omitted

    C->>S: POST /oauth2/register
    S->>DB: GetOAuth2DCREnabled (system ctx, every request, no cache)
    DB-->>S: false
    S-->>C: 403 invalid_request,<br/>"Dynamic client registration is disabled"

    Note over C,S: A client that registered before the change is unaffected
    C->>S: GET /oauth2/authorize?client_id=...
    Note over S: no DCR-enabled check on this path
    S-->>C: 200 (proceeds normally)

    C->>S: PUT/DELETE /oauth2/clients/{client_id} (RFC 7592 self-management)
    Note over S: no DCR-enabled check on this path either
    S-->>C: 200 (proceeds normally)
```

## Files changed: manual vs. generated

Reviewers should focus on the **manual** files. The **generated** ones
are `make gen` output that follows mechanically from the manual changes
and don't need direct review.

<details>
<summary><b>Manual files (26)</b> — click to expand, grouped the same
way as "Suggested review order" below</summary>

**1. Database**

| File | What changed |
|---|---|
| `coderd/database/queries/siteconfig.sql` | New
`GetOAuth2DCREnabled`/`UpsertOAuth2DCREnabled` query pair on the
existing generic `site_configs` table. No schema change. |
| `coderd/database/dbauthz/dbauthz.go` | RBAC check
(`rbac.ResourceDeploymentConfig`) on the two new query methods; extends
the `subjectSystemOAuth2` system-actor role with read-only
`ResourceDeploymentConfig` access, needed so the public
discovery/registration endpoints can read the flag via
`dbauthz.AsSystemOAuth2`. |
| `coderd/database/dbauthz/dbauthz_test.go` | RBAC assertion coverage
for `GetOAuth2DCREnabled`/`UpsertOAuth2DCREnabled` in the
method-coverage test suite. |

**2. Request gating (the actual feature)**

| File | What changed |
|---|---|
| `coderd/oauth2provider/registration.go` | The actual gate:
`CreateDynamicClientRegistration` reads the flag first and returns an
RFC 7591-shaped `403` when disabled (defaults disabled if never
configured). |
| `coderd/oauth2provider/registration_test.go` | New unit test,
`TestCreateDynamicClientRegistration_DCREnabled`: calls the handler
directly (no HTTP server), covering enabled / explicitly disabled /
never-configured. |
| `coderd/oauth2provider/metadata.go` | `GetAuthorizationServerMetadata`
conditionally omits `registration_endpoint` from discovery metadata when
DCR is disabled. |
| `coderd/oauth2provider/metadata_test.go` | New unit test,
`TestGetAuthorizationServerMetadata_DCREnabled`: same three states, for
the discovery handler. |

**3. Admin settings endpoint**

| File | What changed |
|---|---|
| `codersdk/oauth2.go` | New `OAuth2ProviderSettings` SDK type plus
`Client.OAuth2ProviderSettings`/`PutOAuth2ProviderSettings` methods. |
| `coderd/oauth2.go` | New
`oauth2ProviderSettings`/`putOAuth2ProviderSettings` admin handlers
(audited via `audit.InitRequest`); updates the
`GetAuthorizationServerMetadata` call site to pass `api.Database`. |
| `coderd/coderd.go` | Registers `GET`/`PUT
/api/v2/oauth2-provider/settings`. |
| `coderd/oauth2_provider_settings_test.go` | New test file: admin
`GET`/`PUT` round-trip, default-disabled-before-any-`PUT`, and `403` for
a non-owner on both `GET` and `PUT`. |

**4. Audit wiring**

| File | What changed |
|---|---|
| `coderd/database/types.go` | New `database.OAuth2ProviderSettings`
audit-only struct (mirrors `NotificationsSettings`). |
| `coderd/audit/diff.go` | Adds the new struct to the `Auditable` type
union. |
| `coderd/audit/request.go` | Adds the new struct to all four dispatch
switches (`ResourceTarget`, `ResourceID`, `ResourceType`,
`ResourceRequiresOrgID`). |
| `codersdk/audit.go` | New API-facing
`ResourceTypeOAuth2ProviderSettings` constant and its `FriendlyString`
case. |
| `enterprise/audit/table.go` | Field-level audit action map
(`ActionTrack`/`ActionIgnore`) for the new struct. |
|
`coderd/database/migrations/000546_audit_oauth2_provider_settings.up.sql`
| Adds `oauth2_provider_settings` to the `resource_type` Postgres enum,
required for the audit wiring above (`resource_type` is a real enum, not
a Go-only value). |
|
`coderd/database/migrations/000546_audit_oauth2_provider_settings.down.sql`
| No-op (`ALTER TYPE ... ADD VALUE` can't be reverted). |

**5. Test-suite ripple from the disabled-by-default flip**

| File | What changed |
|---|---|
| `coderd/oauth2provider/oauth2providertest/helpers.go` | New shared
test helper, `EnableDCR`, since DCR now defaults to disabled and many
pre-existing tests need it turned on to register a client. |
| `coderd/oauth2_test.go` | Adds
`TestOAuth2DynamicClientRegistrationDisabled` (registers a client,
disables DCR, verifies new registration is rejected while the existing
client's self-management, authorize, and token exchange all keep
working); calls `EnableDCR` in every pre-existing test that registers a
client. |
| `coderd/oauth2_error_compliance_test.go` | Calls `EnableDCR` in every
test that registers a client, so RFC-error-format assertions aren't
masked by the new disabled-by-default gate. |
| `coderd/oauth2_metadata_validation_test.go` | Same: `EnableDCR` added
to every registration-dependent test. |
| `coderd/oauth2_security_test.go` | Same. |
| `coderd/oauth2provider/validation_test.go` | Same (near-duplicate of
`oauth2_metadata_validation_test.go` in a different package). |
| `coderd/oauth2provider/provider_test.go` | Same. |
| `coderd/mcp/mcp_e2e_test.go` | Same, for the MCP end-to-end
dynamic-registration flow test. |

</details>

<details>
<summary><b>Generated files (12)</b> — from <code>make gen</code>, no
need to review directly</summary>

`coderd/apidoc/docs.go`, `coderd/apidoc/swagger.json`,
`coderd/database/dbmetrics/querymetrics.go`,
`coderd/database/dbmock/dbmock.go`, `coderd/database/dump.sql`,
`coderd/database/models.go`, `coderd/database/querier.go`,
`coderd/database/queries.sql.go`, `docs/admin/security/audit-logs.md`,
`docs/reference/api/enterprise.md`, `docs/reference/api/schemas.md`,
`site/src/api/typesGenerated.ts`.

</details>

## Suggested review order

### 1. Database

Establishes the persisted setting and its RBAC rule; everything else
builds on `GetOAuth2DCREnabled`/`UpsertOAuth2DCREnabled`.

1. `coderd/database/queries/siteconfig.sql` — the two new queries. Same
boolean-encoding pattern as the existing
`oauth2_github_default_eligible` key right above them in the same file.
2. `coderd/database/dbauthz/dbauthz.go` — the RBAC wrapper for those two
queries, plus the `subjectSystemOAuth2` role extension (search this file
for `ResourceDeploymentConfig`, it appears in both spots).
3. `coderd/database/dbauthz/dbauthz_test.go` — asserts the RBAC checks
from (2) actually fire.

### 2. Request gating (the actual feature)

Where `POST /oauth2/register` and discovery metadata change behavior.

1. `coderd/oauth2provider/registration.go` — the primary gate. Read this
first; it's the feature.
2. `coderd/oauth2provider/registration_test.go` — its new unit test,
exercising the gate's three states directly against the handler.
3. `coderd/oauth2provider/metadata.go` — the same gating pattern applied
to the discovery `GET` endpoint.
4. `coderd/oauth2provider/metadata_test.go` — its new unit test.

### 3. Admin settings endpoint

How an owner flips the setting live.

1. `codersdk/oauth2.go` — the `OAuth2ProviderSettings` SDK type and
`Client` methods first; this is the public contract everything below
implements against.
2. `coderd/oauth2.go` — the `GET`/`PUT` handlers themselves.
3. `coderd/coderd.go` — route registration, to see where those handlers
get wired in.
4. `coderd/oauth2_provider_settings_test.go` — round-trip and permission
tests.

### 4. Audit wiring

Plumbing required so step 3's `PUT` is auditable; mechanical except for
(3).

1. `coderd/database/types.go` — the audit-only struct; everything else
in this layer exists to plumb it through.
2. `coderd/audit/diff.go` — adds it to the `Auditable` type union (the
compiler enforces this one).
3. `coderd/audit/request.go` — the four dispatch switches; the one part
of this layer worth reading closely.
4. `codersdk/audit.go` — the API-facing resource type constant.
5. `enterprise/audit/table.go` — the field-action map.
6.
`coderd/database/migrations/000546_audit_oauth2_provider_settings.{up,down}.sql`
— read last; a consequence of needing a new `resource_type` enum value
for (1)-(5), not a design decision of its own.

### 5. Test-suite ripple from the disabled-by-default flip

1. `coderd/oauth2provider/oauth2providertest/helpers.go` — the new
`EnableDCR` helper. Read first to understand the fix pattern before
seeing it applied repeatedly.
2. `coderd/oauth2_test.go` — next, since it also contains the new
`TestOAuth2DynamicClientRegistrationDisabled`, not just `EnableDCR` call
sites.
3. The rest, in any order, they're mechanical repeats of the same
one-line addition: `coderd/oauth2_error_compliance_test.go`,
`coderd/oauth2_metadata_validation_test.go`,
`coderd/oauth2_security_test.go`,
`coderd/oauth2provider/validation_test.go`,
`coderd/oauth2provider/provider_test.go`, `coderd/mcp/mcp_e2e_test.go`.

## Explicitly out of scope

Per the design proposal: rate limiting on `POST /oauth2/register`
(tracked separately), retroactively affecting already-registered clients
when DCR is disabled (this only gates new self-registration), and an
Initial Access Token requirement (a separate, follow-up ticket).
2026-07-28 16:59:33 -07:00
J. Scott Miller ce769680ba feat(site): hide workspace resources when lacking workspace-create permission (#27278)
Context: experiment `minimum-implicit-member ` added the ability to set
the default member-roles at a per-organization level. This, along with
the related PR stacked listed below, will be used to enable "gateway
accounts", which are accounts that are entitled to use the AI Gateway
but not create or use workspaces. The Workspaces tab is intentionally
left visible for now.

Hides the "New workspace" button and the empty-state creation CTA on the
Workspaces page for users who cannot create a workspace in any
organization, and guards the creation page itself.

Adds a shared `createWorkspace` authorization check (`workspace`
resource, `create` action, `owner_id: me`, `any_org: true`) to
`site/permissions.json` and threads the result through
`WorkspacesPageView`, `WorkspacesTable`, and `WorkspacesEmpty`. Users
without the permission see an empty state explaining they don't have
permission to create workspaces instead of a dead-end CTA. The create
CTAs on the Templates pages were already gated by per-organization
checks; this brings the Workspaces page in line.

`CreateWorkspacePage` is also gated: it adds an org-scoped
`createWorkspaceForUserID` check to its existing authorization batch and
wraps the view in `RequirePermission`, so a direct URL shows the
standard denial dialog instead of a form that 403s on submit. Users who
can create workspaces for others (`createWorkspaceForAny`) still see the
form.

To see this behavior, enable the experiment. As an admin, visit
Organization -> Roles, and remove "Organization Workspace Access" from
the default roles. Login as a user that is not granted workspace access
via a member role.

Storybook coverage: `CannotCreateWorkspace` (empty state + hidden
button), `CannotCreateWorkspaceWithWorkspaces` (button hidden while the
table renders), `CannotCreateWorkspaceWithFilter` (pins the filter empty
state's priority over the no-permission one), and `PermissionDenied` for
the CreateWorkspacePage gate. The Go SSR permissions test also asserts
the new `createWorkspace` entry.

## Stack

This PR is independent but related to the gateway-accounts stack:

1. **#27279**: permission-based license seat counting. Behind the
`permission-based-licensing` experiment and gated on the AI Governance
add-on, `user_limit` counts only users the RBAC engine authorizes to
create workspaces.
2. **#27280**: adds the `organization-ai-gateway-access` org role
carrying the AI Bridge interception permissions (extracted from the
member floors, backfilled into org default roles by migration) and
enforces it at AI Gateway authentication; bridge usage stops claiming AI
Governance seats under the experiment.
3. ~~**#27281**: gates workspace ACL grants on matching member-level
capability (each granted action only takes effect while the recipient
holds that action in the org), so workspace sharing is ineffective for
(and rejected toward) users without workspace capabilities, evaluated
live on every authorization.~~ Tabled - excluded from the
gateway-accounts MVP.

This PR (#27278) stands alone: it hides the Workspaces page create CTAs
for users without workspace-create permission and can merge in any
order.
2026-07-28 18:09:54 -05:00
dylanhuff-at-coder efbf802319 feat: add bulk secret import upload to Add secret dialog (PLAT-240) (#26725)
Adds a file dropzone to the create branch of the Add secret dialog
(final PR in the PLAT-240 stack, after #26723 and #26724). The browser
reads the file, derives the format from the extension
(`.env`/`.json`/`.yaml`/`.yml`), and imports via `POST /secrets/batch`;
per-entry backend errors surface in an alert and the success toast flags
secrets imported without an env name. Storybook play stories and vitests
cover the flow.

Also documents the upload flow in `docs/user-guides/user-secrets.md`.

Closes https://linear.app/codercom/issue/PLAT-240

> Reviewed and updated by Coder Agents on behalf of @dylanhuff-at-coder.
2026-07-28 15:30:15 -07:00
1a6a8be96c feat: log tailnet tunnels to the connection log (#27423)
Co-authored-by: Chris DiGiamo <cd@anthropic.com>
Co-authored-by: Chris DiGiamo <cdigiamo@anthropic.com>
2026-07-28 15:30:12 -05:00
Andrew Aquino 09a69e624a feat: search users by display name (#27398)
Free-text member search previously matched only username and email, so
typing a person's display name returned no results even though the UI
shows the display name as the primary label. This broadens the free-text
`@search` filter to also match `users.name`.

The change is in three queries: `GetUsers`,
`PaginatedOrganizationMembers`, and `GetGroupMembersByGroupIDPaginated`.
This covers every server-filtered surface: the Users page, the
Organization Members page, the Group Members page, and the
`UserAutocomplete` / `WorkspaceUserAutocomplete` pickers (which query
`GetUsers` with `q`). The org member picker (`MemberAutocomplete`)
filters client-side via cmdk, so display name is added to its
`keywords`.

Explicit filters (`name:`, `username`/`email`) and pagination counts are
unchanged; the group members count still comes from the filtered
`COUNT(*) OVER()` in the same query.

Refs DEVEX-484
Refs DEVEX-565

<details>
<summary>Implementation plan</summary>

## Problem

Member search (both the global Users page and the Organization Members
page) matches only on `username` and `email`. It does not match on the
user's display name (`users.name`), even though the Organization Members
table shows `name` as the primary title. So typing a person's full name
in the search box returns nothing.

Today a bare search term (`alice`) is routed to the SQL `@search`
filter, which only checks `email`/`username`. Display name is only
matched if the user explicitly types `name:alice`, which is
undiscoverable.

## Design decision

Include `name` in the free-text `@search` condition in the affected SQL
queries. A bare term then matches `email OR username OR name`, using the
same case-insensitive substring `ILIKE` already in place. This keeps the
existing explicit `name:` filter working.

Tradeoff: this broadens the meaning of free-text `search` globally
(anything using these queries now also matches display name). This is
the intended behavior, confirmed against DEVEX-565 (display name search
in the user picker).

## Affected files

Backend:
- `coderd/database/queries/users.sql` (`GetUsers`)
- `coderd/database/queries/organizationmembers.sql`
(`PaginatedOrganizationMembers`)
- `coderd/database/queries/groupmembers.sql`
(`GetGroupMembersByGroupIDPaginated`)
- `coderd/database/queries.sql.go` regenerated via `make gen`

Frontend:
- `site/src/components/UserAutocomplete/UserAutocomplete.tsx` (add
`name` to client-side cmdk keywords)

Tests:
- `coderd/coderdtest/users.go` (shared `UsersFilter` helper): added a
`DisplayNameSearch` case and extended search-based expectations to
include `name`. Exercised by `TestGetUsersFilter`,
`TestGetOrgMembersFilter`, and `TestGetGroupMembersFilter`.

Docs:
- `docs/admin/users/index.md`: documented that free-text search matches
username, email, and display name.

## Frontend surface coverage

| Surface | Sends | Backend | Query |
|---|---|---|---|
| Users page | `q` | `GET /users` | `GetUsers` |
| Organization Members page | `q` | paginated members |
`PaginatedOrganizationMembers` |
| Group Members page | `q` | `groupMembers` |
`GetGroupMembersByGroupIDPaginated` |
| User pickers (server-filtered) | `q` | `GET /users` | `GetUsers` |
| Org member picker (client-filtered) | local cmdk | n/a | keyword
change |

## Out of scope

- Trigram/similarity (fuzzy) matching; keeps `ILIKE` substring
semantics.
- Sort/pagination ordering (still `LOWER(username)`).

</details>

---
_Created by Coder Agents on behalf of @aqandrew._
2026-07-28 12:13:58 -07:00
Zach 85984ff142 feat: add enable/disable support for user secrets (#27537)
Users can now disable a secret to stop it from being injected into
workspaces without deleting it, and re-enable it later. Disabled secrets
stay visible and editable everywhere they already appear.

An enabled secret must have at least one injection target; a secret with
no target can be stored only while disabled. Existing target-less secrets
are migrated to disabled to preserve current behavior.

Support spans the REST API, SDK, CLI, dashboard, and audit log.
2026-07-28 09:58:33 -06:00
Jake Howell 5f72c1525e chore(site): demui <ScheduleForm /> component (#27563)
This pull-request deMUIs the `/settings/schedule` page for users. 

| Old | New |
| --- | --- |
| <img width="1041" height="371" alt="PREVIEW_QUIET_HOURS_OLD"
src="https://github.com/user-attachments/assets/15e1e285-a33e-475e-beab-924a53152e00"
/> | <img width="1041" height="406" alt="PREVIEW_QUIET_HOURS_NEW"
src="https://github.com/user-attachments/assets/27daa080-b3e9-4615-9ad3-eefa0d55295b"
/> |
2026-07-29 01:06:38 +10:00
Jake Howell 448fe10d82 fix: remove @mui/material from formUtils.stories.tsx (#27503)
This pull-request removes the material dependency from
`formUtils.stories.tsx`. Just a lil bit of dead code we can clean up 🙂.
2026-07-28 23:39:28 +10:00
Michael Suchacz 8ea2586189 feat: add chat lifecycle hook dispatch backend (#27401)
Adds the chat lifecycle hook wire contract and dispatch plumbing, first
PR of the lifecycle hooks stack (followed by #27428, #27429, #27430).

- `codersdk/x/agenthooks`: event and response wire types, JWT creation
and verification with the shared secret (HS256, request body digest,
expiry and not-before freshness checks), and an HTTP handler helper so
consumers only implement the events they use. The `codersdk/x` location
marks the consumer SDK as experimental.
- `coderd/x/agenthooks/dispatch`: a stateless dispatcher that signs and
posts hook events, enforces a concurrency cap under one configured
timeout that bounds both the capacity wait and both post attempts,
retries one connection failure with the same JWT, sends a distinctive
`coderd-agenthooks/<version>` User-Agent, and records Prometheus
metrics. Delivery is at least once; consumers own durable decision
state, audit records, and deduplication keyed by the stable payload
identifiers. Nothing is persisted by Coder.
- Response bodies decode strictly: unknown fields, duplicate JSON keys
(including inside `input_override`), and trailing data fail the dispatch
closed as protocol errors instead of silently reading as allow.
- `coderd/util/xnet`: shared timeout and connection error classification
used by the dispatcher retry logic. Transient HTTP/2 stream aborts count
as connection errors, so the documented single retry also applies to h2
consumers, which is the shape Go's default transport negotiates against
any TLS consumer. Deterministic protocol failures stay terminal. Only
the struct form of a stream error is matched, because `net/http` bundles
its own HTTP/2 types and `h2_error.go` bridges only that shape.
- `scripts/agenthooks-server`: a reference consumer that logs events and
demonstrates consumer-owned pre-tool decision deduplication. It requires
an explicitly configured JWT audience rather than deriving one from the
request, and its startup output names the mode it is running in so an
operator can see that the example policy flags need `-log-only=false`.
- `scripts/apitypings`: generate TypeScript types for the hook wire
contract.

Dispatch failures log without the error's stack frames, since a failed
dispatch is an expected, operator-visible condition.

Nothing dispatches these events yet; chatd wiring lands in #27429.

> This PR was written by Mux, an AI coding agent, on Mike's behalf.
2026-07-28 13:59:37 +02:00
Susana Ferreira ed37483ff7 feat: add group AI spend endpoint (#27568)
## Description

Adds `GET /api/v2/groups/{group}/ai/spend`, returning the AI spend limit
and aggregate spend for a single group over the current budget period.
The period is derived from the deployment's configured budget period
rather than being caller-specified, matching the other AI spend
endpoints.

## Changes

- Add the `groupAISpend` handler and route, gated by the
`aigateway-cost-control` experiment and the `AIBridge` feature.
- Reuse the existing `GetOrganizationGroupsAISpend` query with a single
group ID, so no new query or authorization path is introduced.
- Add the `GroupAISpend` codersdk type and client method.

Closes
https://linear.app/codercom/issue/AIGOV-475/implement-apiv2groupsgroupaispend

> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by
@ssncferreira
2026-07-28 11:32:51 +01:00
Michael Suchacz e83f018f5f feat(site/src/pages/AISettingsPage/SpendPage): announce cost controls move to AI Governance (#27543)
Adds an informational banner to the AI settings **Spend** tab announcing
that cost controls features move to AI Governance in v2.37, with a link
to the AI Gateway cost controls docs.

Banner copy:

> Cost controls features will move to AI Governance in v2.37. [Read more
here](https://coder.com/docs/ai-coder/ai-gateway/cost-controls)

The link uses the existing `docs()` helper from `#/utils/docs`, so it
resolves the deployment's configured `docs-url` meta tag and otherwise
falls back to a version-pinned `coder.com/docs/@<version>` URL. This
matches how sibling AI settings pages link out (for example
`GatewayKeysPageView` and `ProvidersPageView`).

It points at `/ai-coder/ai-gateway/cost-controls`, the AI Gateway Cost
Controls page added in #27570. That page is present on `main` and the
URL resolves, so the banner links to live documentation.

The banner renders on the main Spend tab. It is intentionally not shown
in the per-user spend drill-in sub-view, which returns early from a
separate component.

## Validation

- Extended the existing `SpendWithLimitsAndUsers` story to assert the
banner copy and the resolved docs `href`. Both assertions were verified
to fail when the banner is removed and when the link points somewhere
else.
- Storybook story tests for `SpendPageView.stories.tsx` pass (12 tests),
plus `tsc -p .`, `biome check`, and `lint:compiler` clean.
- `make pre-commit` passed through the git hooks.

> Mux opened this PR on Mike's behalf.
2026-07-28 12:32:14 +02:00
Jake Howell ea2ad4d6c6 chore(site): premium paywall for <WorkspaceProxyPage /> (#27567) 2026-07-28 18:58:18 +10:00
TJ bfcfb71860 fix: show 'Unset' for missing providers in AI models list (#27400)
## Summary

Frontend-only fixes for the `/ai/settings/models` page:

1. **Provider column displays "Unset"** with an info tooltip when a
model's provider has been deleted, instead of "N/A".
2. **Models without a usable provider display as "Disabled"** in the
list, regardless of the stored `enabled` flag. Covers both missing
(soft-deleted) and disabled providers.
3. **Save button re-enabled when only the provider changes** on the edit
page (previously the button stayed disabled because provider changes
lived outside the formik state).

## Scope

Frontend only. The DB constraint
`chat_model_configs_ai_provider_required_when_active` already prevents a
non-deleted model from having a NULL `ai_provider_id`; CODAGT-709
addresses the server-side cascade when a provider is deleted.

## Changes

- `ModelsPageView.tsx`: two `useMemo` maps (`hasProviderByModelId`,
`providerEnabledByModelId`) passed to `ModelRow`.
- `ModelRow.tsx`: `isEffectivelyEnabled = model.enabled && hasProvider
&& providerEnabled`. When `hasProvider` is false, renders "Unset" with a
standard `InfoIcon` tooltip.
- `ModelForm.tsx`: `canSubmit` OR's in `hasProviderChange` so the save
button enables when only the provider dropdown changes.
- `ModelRow.stories.tsx`: four stories covering baseline,
missing-provider (with tooltip assertion), disabled-provider, and
disabled-model paths.
- `ModelsPageView.stories.tsx`: `OrphanedModelShowsUnset` feeds an
orphaned model through the real derivation (map-miss + `?? false`),
matching the production shape produced by `deriveProviderStates`.
`DisabledProviderModelsStillListed` now asserts the "Disabled" badge.
- `ModelForm.stories.tsx`: `EditUpdateEnabledOnProviderChange` asserts
the save button is enabled when the selected provider differs from the
model's stored provider.
- `testFixtures.ts`: `mockOrphanedModel` fixture representing the
deleted-provider case.

Diff: 7 files, 240 insertions, 9 deletions.

> 🤖 This PR was updated with Coder Agents.
2026-07-28 00:17:45 -07:00
Jake Howell 2794886688 fix: demui <Form /> emotion usage (#27560)
Tiny simple change just moving us over to Tailwind instead of MUI for
`<Form />`. Functionally equivalent minus the mismatch between `md` in
`Tailwind` (`≥ 768px`) and `MUI` (`≥ 900px`).
2026-07-28 04:25:07 +00:00
Jake Howell b448de670b fix(site/src): refresh provider state after device-flow exchange (#26795)
> 🤖 This PR was modified by Coder Agents on behalf of Jake Howell.

Stack:
1. #26575 `fix(site/e2e): close mock external-auth servers in teardown`
2. #26793 `fix(site/e2e): accept 404 from external auth reset hook`
3. #26795 `fix(site/src): refresh provider state after device-flow
exchange` ← this PR
4. #26798 `fix(site/e2e): reset both providers in external auth hook`
5. #26648 `chore(site/e2e): re-enable externalAuth suite`

#18039 (May 2025) upgraded `@tanstack/react-query` from v4 to v5, which
[removed `onSuccess`/`onError`/`onSettled` from
`useQuery`](https://tanstack.com/query/v5/docs/react/guides/migrating-to-v5#callbacks-on-usequery-and-queryobserver-have-been-removed).
The migration updated the `invalidateQueries` argument shape but left
the now-dead `onSuccess` in place on `exchangeExternalAuthDevice`, which
is consumed by `useQuery` in `ExternalAuthPage.tsx`.

The relevant lines from #18039 in
`site/src/api/queries/externalAuth.ts`:

```diff
 		queryKey: ["external-auth", providerId, "device", deviceCode],
 		onSuccess: async () => {
 			// Force a refresh of the Git auth status.
-			await queryClient.invalidateQueries(["external-auth", providerId]);
+			await queryClient.invalidateQueries({
+				queryKey: ["external-auth", providerId],
+			});
 		},
```

Result: after a successful device-flow exchange the
`externalAuthProvider` query is never invalidated, so
`externalAuthProviderQuery.data.authenticated` stays `false` and the UI
is stuck on "Checking for authentication..." until a manual page
refresh. This breaks real users who go through the device flow today,
not just the e2e suite that re-enables in #26648.

This PR drops the dead `onSuccess` (and the `queryClient` param it
depended on), and moves the invalidation into a `useEffect` in
`ExternalAuthPage.tsx` that fires when
`exchangeExternalAuthDeviceQuery.isSuccess` flips true. Matches the
existing pattern in `LoginOAuthDevicePage.tsx`.

Verified against the failing CI run on #26648 ([job
83970095566](https://github.com/coder/coder/actions/runs/28346242963/job/83970095566?pr=26648)):
the trace shows `POST /api/v2/external-auth/device/device` returning
`204` (exchange succeeded) followed by no subsequent `GET
/api/v2/external-auth/device` to refresh the provider state. With this
PR the `isSuccess` effect runs, the provider query refetches, and the UI
flips to the authorized state.

## Regression test

Added `site/src/pages/ExternalAuthPage/ExternalAuthPage.test.tsx` (the
first test for this page). It renders the device flow with a stateful
provider handler that returns `authenticated: false` until the exchange
`POST` lands, then asserts the UI flips from the "Authenticate with
GitHub" polling screen to "You've authenticated with GitHub!" without a
manual refresh, and that the provider endpoint is refetched after the
exchange. Confirmed red against the pre-fix code (stuck on the polling
screen) and green with the fix.

Refs https://linear.app/codercom/issue/DEVEX-413
Refs https://github.com/coder/coder/pull/18039

<details>
<summary>Why <code>useEffect</code> rather than a query-level
callback</summary>

react-query v5 removed `onSuccess`/`onError`/`onSettled` from `useQuery`
because the v4 behaviour was unsound: the callbacks fired per-observer
rather than per-query, so they ran twice when two components observed
the same query and not at all when a component unmounted and cached data
was reused. [TKDodo's "Breaking React Query's API on
Purpose"](https://tkdodo.eu/blog/breaking-react-querys-api-on-purpose)
and the [official v5 migration
guide](https://tanstack.com/query/v5/docs/react/guides/migrating-to-v5#callbacks-on-usequery-and-queryobserver-have-been-removed)
both recommend `useEffect` on `isSuccess` as the replacement.

Alternatives considered:

| Option | Why not |
| --- | --- |
| Side-effect in `queryFn` | Re-adds the `queryClient` dependency we
just removed, and fires on every retry and cached re-read, not just
first success. |
| Convert to `useMutation` | Wrong semantics. This is a polling query
with `retry: isExchangeErrorRetryable` and `retryDelay`; mutations are
one-shot and don't have retry-on-pending machinery. |
| Global `QueryCache` `onSuccess` | Runs for every query in the app;
filtering by queryKey or `meta` for a single per-page side effect is
more code than the effect it replaces. |
| Custom hook wrapping `useQuery` | Only one consumer, so the
abstraction would have one caller. |

Local precedent: `LoginOAuthDevicePage.tsx` already uses the same
`isSuccess` → `useEffect` pattern for the post-success `location.href`
redirect.

</details>

<details>
<summary>Why a separate PR</summary>

Keeps the bisection signal clean. #26575 fixes the EADDRINUSE flake,
#26793 fixes the 404 hook contract drift, this PR fixes the dropped
invalidation, and #26648 just flips `.skip`. Each PR addresses one
independent root cause that piled up while the externalAuth suite was
skipped.

</details>
2026-07-28 03:53:18 +00:00
Jake Howell 072b101624 chore: setup page cleanup (#24649)
This pull-request addresses a few things that may have made this page
less accurate than we would have liked.

* Disable the setup form when it is submitting things to the backend.
* Migrate `<PasswordField />` over to being backed by `<FormField />`
* Don't make use of `<strong />` in the header. Rely on the
`font-semibold` as per other headings in the codebase.
2026-07-28 03:39:42 +00:00
Jake Howell a3d67507e9 fix(site): demui <WorkspaceSettingsForm /> component (#27505)
This pull-request removes the imports of `@mui/material/*` from
`<WorkspaceSettingsForm />`.
2026-07-28 13:19:20 +10:00
TJ 6c916629c9 feat(site): rename "Dismiss warnings" to "Mute warnings" and make health callouts dismissible (#27554)
Fixes a mismatch between the header button's label and its behavior on
the Health pages. Today the button reads **Dismiss warnings**, which
suggests it will close the in-page callout, but it actually toggles
whether the health check surfaces in the top-nav status indicator and
shows a bell-off icon in the sidebar. The callout itself has no way to
be closed.

### Changes
- Rename the toggle to **Mute warnings** / **Unmute warnings** (with
matching toast copy) and rename the component + file from
`DismissWarningButton` to `MuteWarningsButton`.
- Set `dismissible` on the **warning** `<Alert>`s across the Health
pages (Access URL, Database, DERP, DERP region, Provisioner Daemons,
Websocket, Workspace Proxy) so users can close the callout from the
callout itself. `Alert` already supports this via a built-in close
button.
- Error-severity `<Alert>`s are intentionally **not** dismissible:
`HealthLayout` refetches every 30s and reuses the mounted subpage, so
allowing dismissal would suppress subsequent (possibly different) error
messages until reload. Diagnostics pages should not hide active faults.
- Align ProvisionerDaemonsPage's warning callout with the other five
pages by setting `prominent`.

### Notes
- Callout dismissal is client-side only (matches `Alert`'s existing
`useState` behavior). Warning `<Alert>`s are keyed by `warning.code`, so
a dismissed warning reappears on reload/remount but survives a refetch.
The mute toggle continues to persist server-side via
`dismissed_healthchecks`.
- Follow-up filed for a pre-existing UX mismatch: the mute also silently
drops error-severity sections from the top-nav banner (#27557). Kept out
of scope here per requester.
- No API or backend changes.

---

_This PR was generated by Coder Agents on behalf of @tracyjohnsonux._
2026-07-27 19:56:47 -07:00
J. Scott Miller 1ab4ed8db5 feat: exclude AI Bridge usage from AI Governance seat counting (#27280)
Under the new `ai-gateway-seat-exclusion` experiment, AI Bridge usage
stops counting toward AI Governance seats.

## Seat recording

Under the experiment, `RecordInterception` no longer records
`ai_seat_state` usage for the initiator: AI Gateway access is licensed
by the AI Governance add-on rather than per seat. This experiment is
independent of `workspace-capable-licensing` (#27279) so the two
licensing behaviors can be enabled separately. Task workspace builds
still claim AI Governance seats.

## Manual verification

Verified live on a dev deployment (provider chained to dev.coder.com's
gateway, model `gpt-5.6-luna`): with the experiment off, the first
bridge request from each identity type (admin, plain member, service
account) wrote an `ai_seat_state` row (`aibridge` reason); with it on,
requests recorded interceptions but left seat state untouched — no new
rows, and existing rows' `last_used_at` did not advance.

Part of the gateway-accounts feature.

## Stack

Part 2 of the gateway-accounts stack:

1. **#27279**: permission-based license seat counting. Behind the
`workspace-capable-licensing` experiment and gated on the AI Governance
add-on, `user_limit` counts only users the RBAC engine authorizes to
create workspaces.
2. **This PR**: stops AI Bridge usage from claiming AI Governance seats
under the new `ai-gateway-seat-exclusion` experiment.
3. ~~**#27281**: adds a `use_shared` capability precondition for
workspace ACL grants, so workspace sharing is ineffective for (and
rejected toward) users without workspace capabilities, evaluated live on
every authorization.~~ This will be done in follow-up work when we have
time to look into the performance impact.

Related but independent: **#27278** hides the Workspaces page create
CTAs for users without workspace-create permission.
2026-07-27 21:05:37 -05:00
J. Scott Miller 6c102cc3f3 feat: count only workspace-capable users toward license seats (#27279)
Adds permission-based license seat counting behind the
`workspace-capable-licensing` experiment. When the experiment is enabled
and a valid license carries the AI Governance add-on, the `user_limit`
feature counts only active users the RBAC engine authorizes to create a
workspace, instead of every active user. Users without workspace-create
capability ("gateway accounts", e.g. AI-Gateway-only users) no longer
consume seats.

## How it works

- A new `GetActiveUsersAuthorizationRoles` bulk query returns effective
roles (implied member roles, org default member roles) and group
memberships for every seat-eligible user (active, not deleted, not
system, not a service account), matching `GetActiveUserCount` semantics.
- `license.CountWorkspaceCapableUsers` evaluates `workspace.create`
against the any-organization object form, which covers site-wide grants,
membership grants, and org-scoped bans in one check. Evaluation is
deduplicated on a sha256 of each user's canonical subject JSON (a fixed
sentinel user ID, sorted deduplicated roles and groups), so cost scales
with unique subjects rather than user count, and every subject field
participates in both the evaluation and the key.
- The AI Governance add-on is only known after license claims are
parsed, so `Entitlements()` passes a lazy `WorkspaceCapableUserCountFn`
(following the `ManagedAgentCountFn` precedent) and
`LicensesEntitlements` resolves it when a validated add-on is present.
Each license's `user_limit` claim becomes a candidate pair of limit and
counting mode, the most favorable pair is selected (see Behavior notes),
and the selected pair's limit, entitlement, and count become the
`user_limit` feature's terms; the warnings read the same values.
`license.Entitlements` gains `logger`, `authorizer`, and `experiments`
parameters.
- All custom roles are prefetched in a single query before evaluation
(new exported `rolestore.PrefetchCustomRoles`), and each count emits one
Info log line (capable count, eligible active users, unique subjects,
elapsed) whose presence identifies the counting mode. The count is
bounded by a 60s timeout.

## Behavior notes

- Without the experiment or without the add-on, the legacy
`GetActiveUserCount` path is unchanged.
- When the mode is active, the over-limit and expired-limit warnings say
"workspace-capable users" instead of "active users", since that is what
was counted.
- With multiple licenses, each license's `user_limit` claim forms a
candidate pair of limit and counting mode (workspace-capable for add-on
licenses, all active users otherwise), and the most favorable pair is
enforced: a pair satisfied by its own count wins over any unsatisfied
one, then higher entitlement, then higher limit. One license's limit is
never combined with another license's counting mode, so a small add-on
license can neither borrow a bigger non-add-on limit nor suppress it.
- Licenses in their grace period still gate the count; it reverts to the
legacy count only on hard expiry. While the add-on exists only on
grace-period licenses, a warning tells admins the counting mode will
revert and states the legacy active-user count they will then be
measured by.
- Count errors (database failures, timeout) abort the entitlements
computation, matching the legacy count's error semantics: the refresh
fails and the caller keeps the previous entitlements rather than a
silently different count. One exception: a stored role string that fails
to parse is logged and treated as not workspace-capable instead of
failing the refresh, since authorization fails closed on such roles
anyway.
- The experiment is deliberately not in `ExperimentsSafe`.

Part of the gateway-accounts feature; no behavior changes for
deployments without the experiment.

## Stack

Part 1 of the gateway-accounts stack. Each PR builds on the previous:

1. **#27279 (this PR)**: permission-based license seat counting. Behind
the `workspace-capable-licensing` experiment and gated on the AI
Governance add-on, `user_limit` counts only users the RBAC engine
authorizes to create workspaces.
2. **#27280**: adds the `organization-ai-gateway-access` org role
carrying the AI Bridge interception permissions (extracted from the
member floors, backfilled into org default roles by migration) and
enforces it at AI Gateway authentication; bridge usage stops claiming AI
Governance seats under the experiment.
3. ~~**#27281**: gates workspace ACL grants on matching member-level
capability (each granted action only takes effect while the recipient
holds that action in the org), so workspace sharing is ineffective for
(and rejected toward) users without workspace capabilities, evaluated
live on every authorization.~~ Tabled — excluded from the
gateway-accounts MVP.

Related but independent: **#27278** hides the Workspaces page create
CTAs for users without workspace-create permission.

## Benchmarks

`BenchmarkCountWorkspaceCapableUsers` (in `usercount_bench_test.go`, run
manually with `go test ./enterprise/coderd/license/ -bench
BenchmarkCountWorkspaceCapableUsers -benchtime 5x -run '^$'` — never
executed by CI) measures the count across user-scale and role-diversity
shapes:

| Scenario | Users | ~Unique subjects | per count |
|---|---|---|---|
| Uniform | 1k | 4 | 8.5ms |
| Uniform | 10k | 4 | 71ms |
| Uniform | 50k | 4 | 344ms |
| ManyOrgs (100 orgs) | 10k | ~200 | 112ms |
| CustomRoles (1000 org-scoped roles) | 10k | ~1000 | 168ms |
| UniquePairs (every user a distinct subject) | 10k | ~10,000 | 2.66s |

Summary:

- **Row-side cost is ~7µs per user, linear** (role parsing, subject
canonicalization, and sha256 per row). The bulk query + subject dedupe
handles 50k users in ~350ms; extrapolated 100k ≈ 0.7s. A non-issue at
the 10-minute refresh cadence.
- **Unique subjects are the dominant axis at ~0.26ms each** (role
expansion + one any-organization rego evaluation per subject). The
worst-case scenario — every user a distinct subject — costs ~2.7s at 10k
users, extrapolating to ~13s at 50k.
- **Realistic deployments sit near the cheap rows.** Subject diversity
tracks orgs × role/group combinations, not user count; only per-user
custom roles or per-user org-membership patterns approach the worst
case.
- Caveat encountered while building the harness: the roles query's plan
depends on accurate table statistics. With stale stats (e.g. right after
a bulk user import, before autovacuum ANALYZEs), the planner picks a
nested-loop plan that re-runs the aggregation per user row — a ~300×
regression (1.08s for 1k users). Fresh statistics restore the hash-join
plan; the harness ANALYZEs after seeding, so the numbers above reflect
the healthy plan.
2026-07-27 20:43:57 -05:00
McKayla はな 00d134ebfd chore: remove classic parameter UI (#25014) 2026-07-27 19:02:28 -06:00
Yevhenii Shcherbina 2574e6b785 feat: notify admins when a user crosses an AI budget threshold (#27415)
Implements:
https://linear.app/codercom/issue/AIGOV-289/notify-users-and-admins-on-budget-warning-and-limit-reached

Notify admins when a user crosses an AI budget threshold, complementing
the user-facing notifications from
https://github.com/coder/coder/pull/27346

When a priced interception pushes a user's period spend across the
warning (85%) or limit (100%) threshold, the Owners and User Admins now
receive an admin notification naming the affected user, alongside the
user's own notification. The affected user is excluded from the admin
recipients since they already get the user-facing copy. Delivery is
best-effort: a failure to enqueue is logged and never blocks recording
the interception.

The admin templates always show the effective group the spend is
attributed to, and note when the limit comes from a per-user override
rather than the group budget.

Depends on https://github.com/coder/coder/pull/27346

## Screenshots:
<img width="1101" height="440" alt="image"
src="https://github.com/user-attachments/assets/eb731088-05c8-47bd-9d06-fc9d07f63a08"
/>

<img width="468" height="391" alt="image"
src="https://github.com/user-attachments/assets/b89b76a6-3fa8-4735-99a2-43e119a7a7e3"
/>
2026-07-27 17:02:28 -04:00
Jeremy RuppelandCoder Agent 51ac968d5a feat: wire up Template Builder session telemetry endpoint (#27124)
`TemplateBuilderSession` telemetry types and telemetry-server ingestion
were added in earlier PRs (#25082, coder/coder-telemetry-server#41), but
no code ever produced session events. This adds the missing producer.

**Backend**: `POST /api/v2/templatebuilder/sessions` reports wizard
entry and compose completion events directly via
`api.Telemetry.Report()`, using the same inline pattern as
`NetworkEvents` and `UserTailnetConnections`. No database migration or
`createSnapshot()` changes needed. RBAC requires `policy.ActionCreate`
on `ResourceTemplate.AnyOrganization()`, matching the compose endpoint.

**Frontend**: The template builder wizard fires `wizard_entry` on page
mount and `compose_completion` on create success or failure. A
client-generated session ID (UUID) correlates the two events for the
same wizard visit, enabling precise funnel analysis and abandonment
detection in BigQuery. Duration is tracked via `Date.now()` in the
wizard state.

Closes https://linear.app/codercom/issue/DEVEX-599

<details>
<summary>Implementation plan</summary>

## Root Cause Analysis

The DEVEX-599 ticket diagnosis suggested missing DB tables, queries, and
`eg.Go` blocks. That diagnosis assumes the DB-backed periodic snapshot
path is required. It is not. Investigation shows two telemetry reporting
patterns in the codebase:

1. **DB-backed periodic snapshots** (`createSnapshot()` with `eg.Go`
blocks): Used for durable entities like workspaces, templates, users.
2. **Direct inline reporting**
(`api.Telemetry.Report(&telemetry.Snapshot{...})`): Used for ephemeral
events like `NetworkEvents`, `UserTailnetConnections`, `CLIInvocations`.

Template builder sessions are ephemeral events, so the direct inline
reporting pattern is the correct fit.

## Backend Changes

- `codersdk/templatebuilder.go`: `TemplateBuilderSessionRequest` type
with `SessionID`, `EventType` enum, `TemplateBuilderSession()` client
method
- `coderd/coderd.go`: Route registration in `/templatebuilder` group
- `coderd/templatebuilder_handler.go`: Handler with RBAC check, request
validation, session ID fallback, and inline telemetry report
- `coderd/templatebuilder_handler_test.go`: Tests for wizard entry,
compose completion, invalid event type, disabled feature, and member
RBAC rejection

## Frontend Changes

- `site/src/api/api.ts`: `recordTemplateBuilderSession` API method
- `site/src/api/queries/templateBuilder.ts`: React Query mutation
- `site/src/pages/TemplateBuilder/wizardState.ts`: `sessionId` and
`enteredAt` fields, `createWizardState()` factory for per-mount
initialization
- `site/src/pages/TemplateBuilder/TemplateBuilderPageView.tsx`:
`sessionId` prop, `useReducer` initializer form
- `site/src/pages/TemplateBuilder/TemplateBuilderPage.tsx`: Telemetry
calls for wizard entry (on mount) and compose completion (on create
success/failure)

</details>

> 🤖 Generated by Coder Agents

---------

Co-authored-by: Coder Agent <agent@coder.com>
2026-07-27 16:10:40 -04:00
Danielle Maywood 95be28850d refactor(site/src/pages): delete unreachable tool render paths (#27527) 2026-07-27 17:59:08 +01:00
Andrew Aquino 5699f1cdfb fix: retry and cache e2e Coder release downloads to reduce test-e2e ssh flake (#27470)
closes DEVEX-651

## Summary

Fixes coder/internal#218 (`flake: e2e-test / test ssh`).

Despite the title, the `ssh with client v2.8.0` / `ssh with agent
v2.12.1` cases (`site/e2e/tests/outdatedCLI.spec.ts`,
`outdatedAgent.spec.ts`) are not failing because of a bug in SSH. They
fail during **setup**, in `downloadCoderVersion()`, which runs
`install.sh` to fetch an old Coder release from GitHub. Transient GitHub
errors (HTTP 403/503, surfacing as nonzero `curl` exit codes such as 22
or 1) make `install.sh` fail and take the whole ssh test down with it.

This is an external-download flake, confirmed by the recurring
`install.sh failed with code {22,1}` evidence in the issue thread and
Ethan's note ("Networking issues again").

## Changes

1. **Retry-with-backoff** (`site/e2e/helpers.ts`):
`downloadCoderVersion()` now retries `install.sh` up to 5 times with
exponential backoff and jitter (~1s, 2s, 4s, 8s). A single transient
download failure no longer fails the test. `install.sh` already reuses
completed binaries and resumes partial downloads (`curl -C -`), so
retries are cheap.
2. **Cross-run cache** (`.github/workflows/ci.yaml`): the `test-e2e` job
now persists `/tmp/coder-e2e-cache` with `actions/cache`, so most runs
skip the GitHub download entirely. The key is derived from the spec
files that pin the downloaded versions, so it invalidates when those
versions change. Saves are restricted to `main` (`restore` runs
everywhere), matching the existing cache-poisoning convention used for
the Vale and golangci-lint caches.

Before this change, neither retry, mirror, nor cross-run caching
protected this path; the only caching was within a single run.

## Testing

- `biome check e2e/helpers.ts` passes.
- `tsc --noEmit` introduces no new errors.
- CI `test-e2e` exercises the changed path.

<details>
<summary>Investigation notes</summary>

- The failure always originates in `downloadCoderVersion` ->
`install.sh` -> `fetch()` (`curl -#fL ...
https://github.com/coder/coder/releases/download/vX.Y.Z/...`).
- `curl` exit 22 = server returned an HTTP error (403 seen in logs);
exit 1 = other transient failure. GitHub also returned 503s across the
workflow in some occurrences.
- `/tmp/coder-e2e-cache` was not persisted by any `actions/cache` step
in `ci.yaml`, so every fresh job re-downloaded from GitHub and was
exposed to the flake.
- Retry addresses transient failures; the cache removes the dependency
on GitHub for most runs. Combined, they target the root cause at two
layers.

</details>

---

This PR was generated by Coder Agents on behalf of @aqandrew.
2026-07-27 09:40:43 -07:00
Yevhenii ShcherbinaandCian Johnston ce4ee923c2 feat: notify users when AI spend crosses the budget threshold (#27346)
Implements:
https://linear.app/codercom/issue/AIGOV-289/notify-users-and-admins-on-budget-warning-and-limit-reached

Notify users when their AI spend crosses a budget threshold for their
effective group. Two thresholds are covered: a warning at 85%, and a
limit-reached notification at 100%.

Detection runs on the post-response path, right after the interception's
cost is added to the user's daily spend. It reads the user's AI spend on
the same transaction where token usage is recorded and AI daily spend is
incremented, and derives the pre-interception total by subtracting this
interception's cost. In case of `oldSpend < threshold && newSpend >=
threshold` - notification is sent. A single interception that crosses
both thresholds enqueues both notifications.

Detection and delivery are best-effort: a failure is logged and never
fails usage recording. The payload uses only stable values (the
threshold percentage and the spend limit, not the exact spend), so
duplicate enqueues are deduplicated by the notification system.

The two templates are added via migration and appear in each user's
notification settings under the "AI Budget" group.

Admin notifications (owners and user admins) are a follow-up: #27415.

## Screenshots:
<img width="1102" height="252" alt="image"
src="https://github.com/user-attachments/assets/62291510-09ca-4cdf-a1f5-4bdc11a1db4b"
/>

<img width="466" height="384" alt="image"
src="https://github.com/user-attachments/assets/030460ff-6fe2-4d59-b247-3550c543ef30"
/>

---------

Co-authored-by: Cian Johnston <cian@coder.com>
2026-07-27 12:09:21 -04:00
Atif Ali 60c20be46f chore(site): remove beta labels from user secrets dashboard (#27512) 2026-07-27 19:34:38 +05:00
Danielle Maywood fd2faaa2f8 refactor(site/src/pages/AgentsPage): drop defensive code for impossible chat states (#27513) 2026-07-27 14:07:56 +01:00
Jaayden HalkoandCursor 6f2011af88 feat: add chat summary tab in the right sidebar and per-chat cost endpoint (#26649)
Stacked on #26657 (the persisted whole-chat summary backend). Base
branch is `chat-summary-62j9`; review/merge that first.

Adds a reusable `ChatSummary` component.

The summary text is the persisted whole-chat summary (`chat.summary`)
introduced by #26657. It is generated asynchronously and may be `null`
until the first summary is produced, in which case the popover renders a
muted empty state. Live updates arrive via that PR's
`chat_summary_change` watch event, which is already merged into the chat
caches.

Cost is served by a new per-chat endpoint, `GET
/api/experimental/chats/{chat}/cost`, which rolls up assistant-message
cost across a chat's root and child (subagent) chats and is authorized
like the other `{chat}` routes (read on the chat, 404 otherwise).

Visual and interaction coverage lives in `ChatSummary.stories.tsx` and
`ChatSummaryPopover.stories.tsx` (including populated-summary,
empty-state, and cost-loading cases).

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-27 10:05:05 +01:00
Jake Howell 88c7304e0b feat: add AppearanceProvider to decouple externalImages from theme (#27197)
> 🤖 This PR was modified by Coder Agents on behalf of Jake Howell.

## What

Introduces an `AppearanceProvider` / `useAppearance` context that
publishes **user** appearance values derived from the active site theme,
and migrates the current consumers of `theme.externalImages` (`Avatar`,
`ExternalImage`, `IconsPage`) to read from it.

## Why

Today, per-user appearance concerns like `externalImages` are smuggled
onto the Emotion theme object, which forces components to pull in
`useTheme` purely to reach a single appearance value. That couples "how
something looks for this user" to "how the styling engine happens to be
wired", and means every new user-appearance value has to be bolted onto
the theme.

This context gives user appearance a home of its own, decoupled from
Emotion. Components ask for what they actually need (`const {
externalImages } = useAppearance()`) instead of reaching through the
theme.

## Scope: user appearance, not admin appearance

To be explicit, this provider is about **user-level** appearance — the
per-user, theme-derived rendering concerns. It is deliberately *not* the
deployment-level `AppearanceConfig` (application name/logo, service
banners, support/docs links) that admins configure; that is a separate
concern with its own data source and shouldn't be folded in here.

## Future scope

`externalImages` is the **first** value to move here, not the only one.
`Appearance` is deliberately modelled as an open interface so future
*user* appearance state can live in one place without touching every
consumer or overloading the theme again. Likely candidates are other
per-user, theme-derived values, e.g.:

- Terminal font / other typography preferences currently surfaced via
user appearance settings.
- Theme mode and other theme-derived rendering styles that follow the
same "read one value off the theme" pattern as `externalImages`.
- Accessibility-oriented rendering preferences (e.g. reduced motion) as
they're added.

Centralising these behind a single provider keeps consumers stable as
the surface grows and avoids re-litigating the `useTheme` coupling each
time (laziness now, less maintenance later).

## Changes

- Add `site/src/theme/appearance.tsx` (`AppearanceProvider`,
`useAppearance`), defaulting `externalImages` to `forDarkThemes` to
match `DEFAULT_THEME`.
- Wrap children with `AppearanceProvider` in `ThemeOverride` and in the
Storybook preview decorator.
- Migrate `Avatar`, `ExternalImage`, and `IconsPage` off
`theme.externalImages` and onto `useAppearance`.

## Notes

- Kept as a **draft** pending the go-ahead to open for review.
- No behavioural change intended; this is a plumbing/refactor step.
2026-07-27 08:54:28 +00:00
Jake Howell 54bdb48cac fix(site): revert vite bump to 8.0.10 (#27504)
Vite 8.0.14–8.0.16 pulls in a Rolldown regression that emits calls to
`init_emotion_react_browser_development_esm` without importing it, which
crashes the site in `vite`/dev (`Uncaught ReferenceError`).

Revert [#27485](https://github.com/coder/coder/pull/27485) (`8.0.10 →
8.0.16`) back to Vite 8.0.10 until we can bump to Vite >= 8.1.0, which
includes the Rolldown fix.
2026-07-27 18:44:46 +10:00
Jake Howell 9e2fa105de fix: wrong border with in <WorkspaceBuildLogsSection /> (#27507)
I caught this earlier today, annoyed me. `border-width` was coming
through wrong due to the use of just a `border-solid`.

| Old | New |
| --- | --- |
| <img width="1220" height="480" alt="PREVIEW_BUILD_LOGS_OLD"
src="https://github.com/user-attachments/assets/1a2bbf48-3b65-49b6-9a29-8dc1ac587d0d"
/> | <img width="1028" height="475" alt="PREVIEW_BUILD_LOGS_NEW"
src="https://github.com/user-attachments/assets/3dcf00b2-4bcb-4588-adbd-ac219e981813"
/> |
| <img width="220" height="220" alt="build-logs-corner-old"
src="https://github.com/user-attachments/assets/97e03646-2625-4a2b-abfc-0f325fe197d1"
/> | <img width="220" height="220" alt="build-logs-corner-new"
src="https://github.com/user-attachments/assets/4fd90c9a-0487-49b8-a0b3-4c2f05aa8c92"
/> |
2026-07-27 08:38:22 +00:00
McKayla はな 6120fb5988 refactor(site): clean up workspace and template settings layouts (#25209) 2026-07-25 11:45:22 -06:00
dependabot[bot] 320817fa30 chore: bump postcss from 8.5.15 to 8.5.18 in /site (#27486)
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.15 to
8.5.18.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/releases">postcss's
releases</a>.</em></p>
<blockquote>
<h2>8.5.18</h2>
<ul>
<li>Restricted loading previous source maps file to the
<code>opts.from</code> folder for security reasons (use <code>unsafeMap:
true</code> to disable the check).</li>
</ul>
<h2>8.5.17</h2>
<ul>
<li>Fixed <code>Maximum call stack size exceeded</code> error.</li>
<li>Fixed Prototype hijacking for <code>postcss.fromJSON()</code>.</li>
<li>Fixed <code>Input#origin()</code> for unmapped end position (by <a
href="https://github.com/chatman-media"><code>@​chatman-media</code></a>).</li>
</ul>
<h2>8.5.16</h2>
<ul>
<li>Fixed <code>Input#origin()</code> position (by <a
href="https://github.com/mizdra"><code>@​mizdra</code></a>).</li>
<li>Fixed <code>raws</code> after rehydrating a JSON AST (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed putting parent-less node in <code>nodes</code> of new node (by
<a
href="https://github.com/MahinAnowar"><code>@​MahinAnowar</code></a>).</li>
<li>Fixed computing <code>offset</code> in <code>positionBy()</code> (by
<a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a>).</li>
<li>Fixed <code>rangeBy()</code> on <code>index: 0</code> (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/postcss/postcss/blob/main/CHANGELOG.md">postcss's
changelog</a>.</em></p>
<blockquote>
<h2>8.5.18</h2>
<ul>
<li>Restricted loading previous source maps file to the
<code>opts.from</code> folder for security reasons (use <code>unsafeMap:
true</code> to disable the check).</li>
</ul>
<h2>8.5.17</h2>
<ul>
<li>Fixed <code>Maximum call stack size exceeded</code> error.</li>
<li>Fixed Prototype hijacking for <code>postcss.fromJSON()</code>.</li>
<li>Fixed <code>Input#origin()</code> for unmapped end position (by <a
href="https://github.com/chatman-media"><code>@​chatman-media</code></a>).</li>
</ul>
<h2>8.5.16</h2>
<ul>
<li>Fixed <code>Input#origin()</code> position (by <a
href="https://github.com/mizdra"><code>@​mizdra</code></a>).</li>
<li>Fixed <code>raws</code> after rehydrating a JSON AST (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
<li>Fixed putting parent-less node in <code>nodes</code> of new node (by
<a
href="https://github.com/MahinAnowar"><code>@​MahinAnowar</code></a>).</li>
<li>Fixed computing <code>offset</code> in <code>positionBy()</code> (by
<a
href="https://github.com/greymoth-jp"><code>@​greymoth-jp</code></a>).</li>
<li>Fixed <code>rangeBy()</code> on <code>index: 0</code> (by <a
href="https://github.com/sarathfrancis90"><code>@​sarathfrancis90</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/postcss/postcss/commit/4c0d194c136fd374495d0993c890d794cab65b81"><code>4c0d194</code></a>
Release 8.5.18 version</li>
<li><a
href="https://github.com/postcss/postcss/commit/92b4e7891ec7b811821d01acc8aa0f010caf41e2"><code>92b4e78</code></a>
Update dependencies</li>
<li><a
href="https://github.com/postcss/postcss/commit/95663d3eb7ba26f4854dd19d3b4f4425760cf56c"><code>95663d3</code></a>
Limit where source map can be loaded for security reasons</li>
<li><a
href="https://github.com/postcss/postcss/commit/74e25ae9f4efaa56a41a449064a655d7da78072c"><code>74e25ae</code></a>
Release 8.5.17 version</li>
<li><a
href="https://github.com/postcss/postcss/commit/d1518afd5a88f42728b30b87f8917210f363f9f1"><code>d1518af</code></a>
Fix Maximum call stack size exceeded error</li>
<li><a
href="https://github.com/postcss/postcss/commit/2421312ffea96ba77b35ce24a1b2d9c2e22b5e83"><code>2421312</code></a>
Fix linter</li>
<li><a
href="https://github.com/postcss/postcss/commit/a50352c583df991710f92ccac25b36304695161a"><code>a50352c</code></a>
Fix CI</li>
<li><a
href="https://github.com/postcss/postcss/commit/33948f0969bb858acdd52c9692e3a785a3ed0a73"><code>33948f0</code></a>
Prevent prototype hijacking in fromJSON</li>
<li><a
href="https://github.com/postcss/postcss/commit/2131909351161cd2c5fc2be58b14919a873ea824"><code>2131909</code></a>
Update dependencies</li>
<li><a
href="https://github.com/postcss/postcss/commit/93440abcca92793b31c5d1fdf5f2da7b58b27599"><code>93440ab</code></a>
Fix non-closed <code>\&lt;div align=&quot;center&quot;&gt;</code> in
README (<a
href="https://redirect.github.com/postcss/postcss/issues/2110">#2110</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/postcss/postcss/compare/8.5.15...8.5.18">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~GitHub%20Actions">GitHub Actions</a>, a new
releaser for postcss since your current version.</p>
</details>
<br />

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 16:46:48 +00:00
dependabot[bot] dd794101cb chore: bump vite from 8.0.10 to 8.0.16 in /site (#27485)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite)
from 8.0.10 to 8.0.16.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/releases">vite's
releases</a>.</em></p>
<blockquote>
<h2>v8.0.16</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.0.16/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v8.0.15</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.0.15/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v8.0.14</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.0.14/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v8.0.13</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.0.13/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v8.0.12</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.0.12/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
<h2>v8.0.11</h2>
<p>Please refer to <a
href="https://github.com/vitejs/vite/blob/v8.0.11/packages/vite/CHANGELOG.md">CHANGELOG.md</a>
for details.</p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md">vite's
changelog</a>.</em></p>
<blockquote>
<h2><!-- raw HTML omitted --><a
href="https://github.com/vitejs/vite/compare/v8.0.15...v8.0.16">8.0.16</a>
(2026-06-01)<!-- raw HTML omitted --></h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>deps:</strong> reject UNC paths for launch-editor-middleware
(<a
href="https://redirect.github.com/vitejs/vite/issues/22571">#22571</a>)
(<a
href="https://github.com/vitejs/vite/commit/50b951225bbf6151eb84a3ad5a454908ab4a76c9">50b9512</a>)</li>
<li>reject windows alternate paths (<a
href="https://redirect.github.com/vitejs/vite/issues/22572">#22572</a>)
(<a
href="https://github.com/vitejs/vite/commit/dc245c71e5007ea4d891a025e2d69ac96c736546">dc245c7</a>)</li>
</ul>
<h2><!-- raw HTML omitted --><a
href="https://github.com/vitejs/vite/compare/v8.0.14...v8.0.15">8.0.15</a>
(2026-06-01)<!-- raw HTML omitted --></h2>
<h3>Features</h3>
<ul>
<li>send 408 on request timeout (<a
href="https://redirect.github.com/vitejs/vite/issues/22476">#22476</a>)
(<a
href="https://github.com/vitejs/vite/commit/c85c9eeb9aaf41f477b48b057146887bd5620797">c85c9ee</a>)</li>
<li>update rolldown to 1.0.3 (<a
href="https://redirect.github.com/vitejs/vite/issues/22538">#22538</a>)
(<a
href="https://github.com/vitejs/vite/commit/646dbedd2870f8ec48df0321177d8aa64bbd1575">646dbed</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li>capitalize error messages and remove spurious space in parse error
(<a
href="https://redirect.github.com/vitejs/vite/issues/22488">#22488</a>)
(<a
href="https://github.com/vitejs/vite/commit/85a0eff1c82bbb7c99a0fe8e63704316578a40d3">85a0eff</a>)</li>
<li><strong>deps:</strong> update all non-major dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/22511">#22511</a>)
(<a
href="https://github.com/vitejs/vite/commit/2686d7d0b722402204d3bcc687a87adea1bcf9fa">2686d7d</a>)</li>
<li><strong>dev:</strong> fix html-proxy cache key mismatch for /@fs/
HTML paths (<a
href="https://redirect.github.com/vitejs/vite/issues/21762">#21762</a>)
(<a
href="https://github.com/vitejs/vite/commit/47c4213f134f562c41ed7c031e4788510cf7e31e">47c4213</a>)</li>
<li><strong>glob:</strong> error on relative glob in virtual module when
no files match (<a
href="https://redirect.github.com/vitejs/vite/issues/22497">#22497</a>)
(<a
href="https://github.com/vitejs/vite/commit/5c8e98f8b584ac5d42f0f9b8580c49792213b13c">5c8e98f</a>)</li>
<li><strong>optimizer:</strong> close the rolldown bundle when write()
rejects (<a
href="https://redirect.github.com/vitejs/vite/issues/22528">#22528</a>)
(<a
href="https://github.com/vitejs/vite/commit/e3cfb9deecff563550fa1b8abd27656b8b292815">e3cfb9d</a>)</li>
<li><strong>resolve:</strong> provide onWarn for viteResolvePlugin in JS
plugin containers (<a
href="https://redirect.github.com/vitejs/vite/issues/22509">#22509</a>)
(<a
href="https://github.com/vitejs/vite/commit/40985f1c09b7696e594e6c5695fbc315d2da2c83">40985f1</a>)</li>
</ul>
<h3>Miscellaneous Chores</h3>
<ul>
<li><strong>deps:</strong> update rolldown-related dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/22566">#22566</a>)
(<a
href="https://github.com/vitejs/vite/commit/3052a67d9350f4c5076ab1c222c4a21a589cbcdd">3052a67</a>)</li>
</ul>
<h3>Code Refactoring</h3>
<ul>
<li>correct logic in <code>collectAllModules</code> function (<a
href="https://redirect.github.com/vitejs/vite/issues/22562">#22562</a>)
(<a
href="https://github.com/vitejs/vite/commit/6978a9ceb942c4f5e211d52b8a1e569f8a65c80c">6978a9c</a>)</li>
</ul>
<h2><!-- raw HTML omitted --><a
href="https://github.com/vitejs/vite/compare/v8.0.13...v8.0.14">8.0.14</a>
(2026-05-21)<!-- raw HTML omitted --></h2>
<h3>Features</h3>
<ul>
<li>update rolldown to 1.0.2 (<a
href="https://redirect.github.com/vitejs/vite/issues/22484">#22484</a>)
(<a
href="https://github.com/vitejs/vite/commit/96efc88570b6a6ddf1a910f106920cbac07b3cf0">96efc88</a>)</li>
</ul>
<h3>Bug Fixes</h3>
<ul>
<li><strong>deps:</strong> update all non-major dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/22471">#22471</a>)
(<a
href="https://github.com/vitejs/vite/commit/98b81632139d51820f82036e58d6fbbf122b77b3">98b8163</a>)</li>
<li><strong>dev:</strong> handle errors when sending messages to vite
server (<a
href="https://redirect.github.com/vitejs/vite/issues/22450">#22450</a>)
(<a
href="https://github.com/vitejs/vite/commit/e8e9a34dcf2540139de558a10187630884d10217">e8e9a34</a>)</li>
<li><strong>html:</strong> handle trailing slash paths in
transformIndexHtml (<a
href="https://redirect.github.com/vitejs/vite/issues/22480">#22480</a>)
(<a
href="https://github.com/vitejs/vite/commit/5d94d1bffdb2a15de9341194d89baec86ce1f693">5d94d1b</a>)</li>
<li><strong>optimizer:</strong> pass oxc jsx options to transformSync in
dependency scan (<a
href="https://redirect.github.com/vitejs/vite/issues/22342">#22342</a>)
(<a
href="https://github.com/vitejs/vite/commit/b3132dacea9c6e0cf526cd9f0f09d850f577c262">b3132da</a>)</li>
</ul>
<h3>Miscellaneous Chores</h3>
<ul>
<li><strong>deps:</strong> update rolldown-related dependencies (<a
href="https://redirect.github.com/vitejs/vite/issues/22470">#22470</a>)
(<a
href="https://github.com/vitejs/vite/commit/7cb728eb629cc677661f1bc52a044ffc0b87fc7f">7cb728e</a>)</li>
<li>remove irrelevant commits from changelog (<a
href="https://github.com/vitejs/vite/commit/2c69495f250edf01132d4a20128de19dbe836086">2c69495</a>)</li>
</ul>
<h3>Code Refactoring</h3>
<ul>
<li><strong>glob:</strong> do not rewrite import path for absolute base
(<a
href="https://redirect.github.com/vitejs/vite/issues/22310">#22310</a>)
(<a
href="https://github.com/vitejs/vite/commit/0ae2844ab6d6d1ccf78a2975b8132769fc35b302">0ae2844</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vitejs/vite/commit/f94df87ff03b40b65e29bacdc04cc18c7bccaa4a"><code>f94df87</code></a>
release: v8.0.16</li>
<li><a
href="https://github.com/vitejs/vite/commit/dc245c71e5007ea4d891a025e2d69ac96c736546"><code>dc245c7</code></a>
fix: reject windows alternate paths (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22572">#22572</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/50b951225bbf6151eb84a3ad5a454908ab4a76c9"><code>50b9512</code></a>
fix(deps): reject UNC paths for launch-editor-middleware (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22571">#22571</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/8d1b0195fd186d0b3297d7cd17acff6c96797420"><code>8d1b019</code></a>
release: v8.0.15</li>
<li><a
href="https://github.com/vitejs/vite/commit/2686d7d0b722402204d3bcc687a87adea1bcf9fa"><code>2686d7d</code></a>
fix(deps): update all non-major dependencies (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22511">#22511</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/3052a67d9350f4c5076ab1c222c4a21a589cbcdd"><code>3052a67</code></a>
chore(deps): update rolldown-related dependencies (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22566">#22566</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/e3cfb9deecff563550fa1b8abd27656b8b292815"><code>e3cfb9d</code></a>
fix(optimizer): close the rolldown bundle when write() rejects (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22528">#22528</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/6978a9ceb942c4f5e211d52b8a1e569f8a65c80c"><code>6978a9c</code></a>
refactor: correct logic in <code>collectAllModules</code> function (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22562">#22562</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/646dbedd2870f8ec48df0321177d8aa64bbd1575"><code>646dbed</code></a>
feat: update rolldown to 1.0.3 (<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22538">#22538</a>)</li>
<li><a
href="https://github.com/vitejs/vite/commit/85a0eff1c82bbb7c99a0fe8e63704316578a40d3"><code>85a0eff</code></a>
fix: capitalize error messages and remove spurious space in parse error
(<a
href="https://github.com/vitejs/vite/tree/HEAD/packages/vite/issues/22488">#22488</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/vitejs/vite/commits/v8.0.16/packages/vite">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=vite&package-manager=npm_and_yarn&previous-version=8.0.10&new-version=8.0.16)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts page](https://github.com/coder/coder/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 16:35:24 +00:00
dependabot[bot] 5fbb9c978d chore: bump react-router from 7.15.1 to 7.18.0 in /site (#27484)
Bumps
[react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router)
from 7.15.1 to 7.18.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/remix-run/react-router/releases">react-router's
releases</a>.</em></p>
<blockquote>
<h2>v7.18.0</h2>
<p>See the changelog for release notes: <a
href="https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v7180">https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v7180</a></p>
<h2>v7.17.0</h2>
<p>See the changelog for release notes: <a
href="https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v7170">https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v7170</a></p>
<h2>v7.16.0</h2>
<p>See the changelog for release notes: <a
href="https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v7160">https://github.com/remix-run/react-router/blob/main/CHANGELOG.md#v7160</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/remix-run/react-router/blob/main/packages/react-router/CHANGELOG.md">react-router's
changelog</a>.</em></p>
<blockquote>
<h2>v7.18.0</h2>
<h3>Patch Changes</h3>
<ul>
<li>Fix server handler prerender responses when using <code>ssr:
false</code> and <code>future.v8_trailingSlashAwareDataRequests:
true</code>. Avoids false positive &quot;SPA Mode&quot; detection when
serving prerendered paths (<a
href="https://redirect.github.com/remix-run/react-router/pull/15173">#15173</a>)</li>
<li>Use the <code>ServerRouter</code> nonce for nonce-aware SSR
components when they don't provide their own value so strict CSP pages
can load them. (<a
href="https://redirect.github.com/remix-run/react-router/pull/15170">#15170</a>)</li>
<li>Use <code>turbo-stream</code> to serialize and deserialize Framework
Mode hydration errors (<a
href="https://redirect.github.com/remix-run/react-router/pull/15175">#15175</a>)</li>
<li>Precompute route branch matchers to avoid recompiling route path
regexes during matching (<a
href="https://redirect.github.com/remix-run/react-router/pull/15186">#15186</a>)</li>
<li>Use the constructed request URL host when validating action request
origins. (<a
href="https://redirect.github.com/remix-run/react-router/pull/15185">#15185</a>)</li>
<li>Remove the un-documented custom error serialization logic from Data
Mode SSR built-in hydration flows (<a
href="https://redirect.github.com/remix-run/react-router/pull/15175">#15175</a>)</li>
<li>Validate protocols in RSC render redirects (<a
href="https://redirect.github.com/remix-run/react-router/pull/15177">#15177</a>)</li>
<li>Consolidate url normalization logic and better handle mixed slashes
(<a
href="https://redirect.github.com/remix-run/react-router/pull/15176">#15176</a>)</li>
</ul>
<h2>v7.17.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>Ship a subset of the official documentation inside the
<code>react-router</code> package (<a
href="https://redirect.github.com/remix-run/react-router/pull/15121">#15121</a>)
<ul>
<li>Markdown docs are now available in
<code>node_modules/react-router/docs</code>, letting AI coding agents
and the React Router agent skills read official docs locally</li>
<li>Excludes auto-generated API docs (<code>api/</code>),
<code>community/</code> content, and tutorials
(<code>tutorials/</code>)</li>
</ul>
</li>
</ul>
<h2>v7.16.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>Stabilize
<code>future.unstable_trailingSlashAwareDataRequests</code> as
<code>future.v8_trailingSlashAwareDataRequests</code> (<a
href="https://redirect.github.com/remix-run/react-router/pull/15098">#15098</a>)</li>
</ul>
<h3>Patch Changes</h3>
<ul>
<li>
<p>Disable manifest path when lazy route dicovery is disabled (<a
href="https://redirect.github.com/remix-run/react-router/pull/15068">#15068</a>)</p>
</li>
<li>
<p>Fix browser URL creation to use the configured history window instead
of the global window. (<a
href="https://redirect.github.com/remix-run/react-router/pull/15066">#15066</a>)</p>
<ul>
<li>Pass the history/router window through to
<code>createBrowserURLImpl</code> so custom window contexts keep the
correct URL origin.</li>
</ul>
</li>
<li>
<p>Fix <code>useNavigation()</code> return type to preserve
discriminated union across navigation states (<a
href="https://redirect.github.com/remix-run/react-router/pull/15095">#15095</a>)</p>
</li>
<li>
<p>Widen <code>MetaDescriptor</code> <code>script:ld+json</code> type
from <code>LdJsonObject</code> to <code>LdJsonObject |
LdJsonObject[]</code> to permit multiple JSON-LD schemas in a single
<code>&lt;script type=&quot;application/ld+json&quot;&gt;</code> tag
emitted by <code>&lt;Meta /&gt;</code> (<a
href="https://redirect.github.com/remix-run/react-router/pull/15082">#15082</a>)</p>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/remix-run/react-router/commit/6fb1e79f8304eddd8b78759edea83cb32389ebf5"><code>6fb1e79</code></a>
Release v7.18.0 (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router/issues/15187">#15187</a>)</li>
<li><a
href="https://github.com/remix-run/react-router/commit/09e6020d1950e54f361f7ad00938ecd4dde60929"><code>09e6020</code></a>
Optimize route matching internals (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router/issues/15186">#15186</a>)</li>
<li><a
href="https://github.com/remix-run/react-router/commit/5b57f5f371595ad97ac91cca389c5adc08ddcc3a"><code>5b57f5f</code></a>
Request Host derivation + CSRF check simplifications (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router/issues/15185">#15185</a>)</li>
<li><a
href="https://github.com/remix-run/react-router/commit/ce596e823f0d7b883a433af1d5a839a8b9fe0242"><code>ce596e8</code></a>
Validate RSC redirect protocols (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router/issues/15177">#15177</a>)</li>
<li><a
href="https://github.com/remix-run/react-router/commit/1cebd2a823bb232ad74dcb2d970f750070b2bebe"><code>1cebd2a</code></a>
chore: format</li>
<li><a
href="https://github.com/remix-run/react-router/commit/9d22943fd46c8ae4b08236425fa3549e10e9ad1a"><code>9d22943</code></a>
Use turbo stream for framework hydration errors (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router/issues/15175">#15175</a>)</li>
<li><a
href="https://github.com/remix-run/react-router/commit/bf63729561365b50705a24fd576e293424df23ef"><code>bf63729</code></a>
Consolidate url normalization logic (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router/issues/15176">#15176</a>)</li>
<li><a
href="https://github.com/remix-run/react-router/commit/4ce8ff72737bcf43afc7e9f5705c9214a19ec9f6"><code>4ce8ff7</code></a>
Fix prerendering pathname issue with trailingSlashAwareDataRequests e…
(<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router/issues/15173">#15173</a>)</li>
<li><a
href="https://github.com/remix-run/react-router/commit/4f060dd11cd8bedcba9e3ee96fce832bd987fe25"><code>4f060dd</code></a>
Use ServerRouter nonce when nonce prop is not specified (<a
href="https://github.com/remix-run/react-router/tree/HEAD/packages/react-router/issues/15170">#15170</a>)</li>
<li><a
href="https://github.com/remix-run/react-router/commit/3fce6d67f805a76b9c3e5a2f0352847ffb3355d2"><code>3fce6d6</code></a>
Update docs on data router singleton</li>
<li>Additional commits viewable in <a
href="https://github.com/remix-run/react-router/commits/react-router@7.18.0/packages/react-router">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=react-router&package-manager=npm_and_yarn&previous-version=7.15.1&new-version=7.18.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts page](https://github.com/coder/coder/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 16:32:14 +00:00
McKayla はな b8727d9c23 fix: don't show admin settings dropdown to everyone (#27481) 2026-07-23 22:59:40 -06:00
McKayla はな 4f50b77ac5 fix: tweak Pill styles (#27475) 2026-07-23 19:03:00 -06:00
McKayla はな 3cf97ff8e7 fix: show selected owner's external auth when creating a workspace (#26653) 2026-07-23 16:39:53 -06:00
dylanhuff-at-coder d5a3963167 feat: add bulk user secret import endpoint and SDK client (PLAT-240) (#26724)
Adds `POST /api/v2/users/{user}/secrets/batch` and
`codersdk.Client.ImportUserSecrets` to import env, JSON, or YAML secrets
atomically. The endpoint validates each entry, rolls back the full batch
on conflicts or limits, omits secret values from responses and audit
logs, and imports keys that cannot be injected as environment variables
with an empty `env_name`.

Part of the [PLAT-240 bulk secret import
stack](https://linear.app/codercom/issue/PLAT-240). Reviewed and updated
by Coder Agents on behalf of @dylanhuff-at-coder.
2026-07-23 14:55:34 -07:00
Ehab Younes 10624122c5 feat(site/src): show spend for unlimited and zero AI budgets (#27458)
The group members table hid a member's spend behind a bare "Unlimited"
label when their budget resolves to a group with no limit, and rendered
a $0 budget as a special "None" label. Spend now always shows: as
"$X / Unlimited USD" for unlimited budgets, and as a normal limit row
("Group limit $0", exceeded color once spend is above zero) for $0
budgets.

The Everyone badge drops "(not allocated)" when the Everyone group's
own budget or a user override applies, showing "Everyone" or
"Everyone (individual)" instead. The not-attributed tooltip now states
that the amount is the user's spend in the viewed group and that their
AI budget is managed by another group, replacing the misleading
"Not attributed to this group" wording.
2026-07-23 17:38:05 +00:00
Jaayden HalkoandCursor 3c7a1d33e3 feat: add persisted whole-chat summary with background generation (#26657)
Adds a persisted whole-chat summary that backs the chat summary popover.
A new nullable `chats.summary` column is populated in the background
after a successful root-chat turn and pushed to clients via a new
`chat_summary_change` watch event (distinct from `summary_change`, which
is bound to `last_turn_summary`), so the popover reads `chat.summary`
straight off the loaded `Chat` with no extra query.

This is the data source for the popover and per-chat cost UI built in
#26649; the popover can consume `chat.summary` once this lands (the
field is nullable, so merge order does not matter).

## How it works

- **Generation** runs in the existing successful-turn finalize hook,
detached from the request so the user's turn is never blocked. A cadence
gate generates the first summary after one completed turn, then
regenerates every three turns, using the `chats.summary_generated_at`
freshness marker. Generation reads compaction-aware history, renders it
to a bounded plain-text transcript (short transcripts are skipped), and
asks for a 1-3 sentence summary via structured output. Failures never
clear an existing summary.
- **Staleness** is guarded by `history_version` (mirroring
`last_turn_summary`), so a background write racing a newer turn loses
while worker lifecycle transitions cannot reject a fresh write.
- **Model selection** uses the chat's configured model.

## Deferred to follow-ups

- **Cost accounting**: the `chat_messages.cost_source` discriminator and
summary/title usage recording were removed from this PR so summary
persistence is not blocked by hidden accounting rows advancing
`history_version`. Title usage recording stays on main's
`InsertChatMessages` path.
- **Model override**: deployment-wide summary generation model selection
is split into #26803; the base feature always uses the chat model.

## Notes

- Migration `000540` adds `chats.summary` and
`chats.summary_generated_at`, and recreates `chats_expanded` to expose
the new columns.
- Root chats only; shared viewers pick up the summary on their next
refetch (live watch events are owner-only).

Refs #26649

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 16:36:23 +01:00