Commit Graph
360 Commits
Author SHA1 Message Date
Danielle Maywood 8405bbb26c feat(site): show best-effort model pricing on the model form (#28290) 2026-08-19 11:45:30 +01:00
Michael Suchacz 119f2b1dd9 feat: limit concurrent chat agents with pooled admission (#27902)
Limits concurrent chat generation on capped deployments to 5 root chats
and 10 delegated subagent chats. The pools are deployment-wide and
independent, so delegated work can continue while root capacity is full.

The default caps live in AGPL code. Enterprise contributes only a
licensing unlock, so unlicensed deployments stay capped and cannot fail
open. Licensed deployments are uncapped while Agent Hours usage stays
below an explicit hard limit. Deployments without a hard limit remain
uncapped, and reaching the Agent Hours allocation only triggers
warnings.

Admission happens before a worker takes chat ownership. Capped
deployments serialize admission across replicas with a
transaction-scoped advisory lock and derive active and queued state from
current ownership plus fresh runner heartbeats, rather than persisted
queue markers or per-replica state. The acquisition query returns a
bounded, pool-interleaved candidate set instead of ranking the whole
backlog; a migration replaces the acquisition index with a pool-aware
one. Refused chats stay running but unowned, and interrupt requests
bypass admission so users can stop queued or over-cap chats.

The single-chat API derives `queued_for_capacity` from live pool state;
list endpoints do not report it. The UI polls that value every 5 seconds
while a chat is running and shows a callout when the chat is waiting for
capacity.

Updates the administrator documentation and deployment-wide Prometheus
gauges for active and queued agents. Replica-level values must be
aggregated with `max`, not `sum`.

> Mux updated this PR on Mike's behalf.
2026-08-18 16:55:43 +02:00
Michael Suchacz 521c383f6b fix: repair stale chat agent bindings after workspace rebuild (#28152)
## Problem

When a chat is bound to a workspace, chatd persists `chats.agent_id`
pointing at a specific workspace agent, and it only rebinds on the next
chat turn. A workspace stop/start creates a new agent with a new ID in
the latest build, so the chat page resolves the stale agent ID to
`undefined` and the right sidebar silently drops Terminal, Desktop,
Browser, apps, and ports even though the workspace is running. The
existing read-time enrichment only filled nil agent IDs and skipped
stale non-nil ones, so refreshing did not help until the user sent
another message.

## Fix

- `coderd/exp_chats.go`: single-chat reads now repair agent IDs that no
longer resolve in the workspace's latest build, using the same
`agentselect.FindChatAgent` selection chatd uses. A repaired binding
also carries the latest build's ID so the response never pairs the new
agent with the previous build. Bindings that still resolve are
preserved, and repair stays best-effort and response-only (no
write-on-read). List reads keep the previous nil-fill-only behavior
because validating existing bindings would cost a per-workspace
authorization lookup per listed chat.
- `site/src/pages/AgentsPage/AgentChatPage.tsx`: the workspace watch
update handler detects when a running workspace's latest build no longer
contains the chat's bound agent and invalidates the chat query once per
chat/build/binding key for immediate recovery, and the chat query polls
every 30 seconds while the binding remains unresolved so a transiently
failed repair retries even when an idle workspace publishes no further
watch events. The watch stream replays the current workspace on every
(re)connect, so this covers rebuilds that happen while the page is open
or disconnected; page loads are covered by the server-side repair. The
workspace-watcher bailout now also keys on `latest_build.id` so a
rebuild propagates while the page is open.
- `site/src/api/queries/chats.ts`: chat watch events replay the
persisted (pre-repair) binding, so the summary merge adopts a snapshot's
`build_id` only when the snapshot agrees on `agent_id`, keeping the
repaired agent/build pair atomic in the caches.

## Testing

- `go test ./coderd -run TestEnrichChatAgentIDs` covering repair,
keep-valid, selection-error, list-mode-skips-bound, and no-workspaces
cases.
- Storybook interaction story `RecoversSidebarAfterWorkspaceRebuild`
exercising the watch-event to chat-refetch to sidebar-recovery flow
(verified red without the invalidation, green with it).
- `pnpm test AgentChatPage.test.ts` covering the binding-resolution
predicate.

> Mux created this PR on Mike's behalf.
2026-08-16 20:30:32 +02:00
Michael Suchacz d509e1e6a0 fix(site): include owner context in Agents org picker permission check (#28076)
Fixes the Agents org picker and workspace attach menu for users whose
only chat grant comes from the member-scoped "Coder Agents User"
(`agents-access`) org role (PRODUCT-552).

## Problem

The per-org authcheck behind the org picker checked `chat:create` with
only `organization_id`. The `agents-access` role grants chat permissions
at org-member scope, which requires the checked object to be owned by
the caller (`policy.rego` `org_member` requires a non-empty owner
matching the subject). With no `owner_id`, every org check returned
`false`, so:

- the org picker never rendered (`permittedOrgs.length > 1` gate),
- the form stayed pinned to the default org,
- the workspace attach menu, filtered to that org, showed "No workspaces
found" even though the user had workspaces in another org.

The page-level `createChat` check already includes `owner_id: "me"`,
which is why the same user could load the page and create chats via the
API.

## Fix

Pass `owner_id: "me"` in the `permittedOrganizations` seed check in
`AgentCreateForm`, matching the page-level check's semantics. The
`permittedOrganizations` helper spreads the check object through, so
each per-org check now carries owner context and the backend substitutes
the caller's user ID.

The other `permittedOrganizations` callsites
(`organization_member:create`, `template:create`) check org-scoped admin
permissions and correctly omit `owner_id`.

Adds a regression story whose `checkAuthorization` mock only allows
checks carrying `owner_id: "me"` (mirroring the RBAC member-scope
behavior); it fails without the fix and passes with it (red-green
verified).

## Validation

- Red-green verified regression story: fails without the fix (picker not
found), passes with it; all 31 stories in the file pass.
- `pnpm -C site check` and `pnpm -C site lint:types` clean.
- Remote dogfood UAT (dev.coder.com chat
[97be7f39](https://dev.coder.com/agents/97be7f39-4688-45ee-be3d-24bc4f6f8046)):
PASS on all acceptance criteria at this exact commit. Reproduced the bug
scenario end to end (two orgs, non-admin user with only the "Coder
Agents User" role in both, workspace only in the second org): the org
picker renders, the second org's workspaces appear in the attach menu,
and chat creation succeeds with a real model. Single-org and admin
behaviors unchanged. Authcheck probe documents the backend semantics:
`chat:create` with `owner_id: "me"` returns true, without it returns
false.

> Mux acted on Mike's behalf for this PR.
2026-08-13 11:19:38 +02:00
Danielle Maywood c424a76a12 feat: wire chat search box to full-text search (#27973) 2026-08-12 15:04:01 +01:00
Andrew Aquino 87fdd2b59b feat(site): add server-side search and pagination for groups page (#27994)
Repeat of #27604, which I had mistakenly manually merged into #27603,
instead of letting Graphite handle it after #27603 merges into main.
Reattempting with GitHub stacks now instead of Graphite!

See #27604 for details/review.
2026-08-10 13:23:14 -07:00
Danielle Maywood 192842c8f2 fix(site/src): treat chat deleted watch events as archive instead of eviction (#27921) 2026-08-10 09:29:38 +01:00
Danielle Maywood 19fdc23d63 fix(site/src): fan chat message upserts out to every containing page (#27912) 2026-08-10 09:08:05 +01:00
Danielle Maywood f9047e5836 feat(site/src): reconcile chats-by-workspace cache across archive and watch paths (#27901)
Implements Phase 2 item 5 of the chats query architecture: by-workspace
cache reconciliation. Stacked on #27892; base branch
`feat/chat-search-invalidation`.

## Problem

`chatsByWorkspace` mappings (flat `Record<workspaceId, chatId>`, IDs
only, archived chats filtered server-side) go stale on
archive/unarchive, workspace binding changes, and watch events (FINDINGS
2.3). Because the client cannot re-derive archived state from the cached
map, the only correct repair for a stale mapping is synchronous removal
plus family invalidation.

## Fix

- New `removeChatFromChatsByWorkspace(queryClient, chatId)` in
`site/src/api/queries/chats.ts`: value-match removal across the
by-workspace family, reference-preserving when nothing is removed
(mirrors the `patchChatMessages` no-op pattern).
- `archiveChat.onSuccess` and the AgentsPageLayout archive-and-delete
`onSuccess` synchronously remove the mapping; the existing
`onSettled`/explicit invalidations then reconverge.
- Watch handler: `deleted` branch removes then invalidates
(remove-before-invalidate ordering); `created` root branch invalidates;
merge path invalidates behind a new `shouldInvalidateChatsByWorkspace`
predicate (`status_change`, `action_required` only; `created`/`deleted`
have their own branches, title/summary/diff/context events do not move
`updated_at` ordering); `onOpen` reconnect invalidates for convergence.
- `useChatToolInvalidations`: workspace-binding tool completion
(`create_workspace`) also invalidates by-workspace; this is the only
reconciliation path on the embed route.

## Confirmed exclusions

- No optimistic patch in `updateChatWorkspace.onMutate`; the awaited
`onSettled` invalidation already converges.
- No cancellation guard for the by-workspace family (Phase 2 item 9
territory).
- No `createChat` change (already invalidates); no ACL or pin changes.

## Known constraints

- Cascade archives remove only the event's own chat ID from the mapping;
per-family-member `deleted` events plus family invalidation repair the
rest.
- The REST `patchChat` workspace-rebind branch publishes no watch event
server-side, so cross-session manual rebinds converge only via the
acting session's `onSettled` or a WorkspacesPage remount; not fixable
client-side.

## Testing

- `chats.test.ts`: removal scoping and reference-preservation tests;
`it.each` wiring tests (archiveChat/unarchiveChat/updateChatWorkspace
onSettled, createChat onSuccess invalidate by-workspace);
synchronous-removal assertion for `archiveChat.onSuccess`; negative
assertions for `updateChatTitle` and `createChatMessage`.
- `AgentsPageLayout.test.ts`: exhaustive `ChatWatchEventKind` table for
`shouldInvalidateChatsByWorkspace`.
- `useChatToolInvalidations.test.tsx`: create_workspace regression test
extended with a seeded by-workspace bystander.

PR generated by Coder Agents.
2026-08-06 10:25:37 +01:00
Danielle Maywood 632eecc090 feat(site/src): reintroduce chat search cache invalidation (#27892)
Stacked on `feat/chat-cache-semantic-ops`.

## Bug

Chat search results (`chatSearch` queries) were never invalidated, so
the search dialog served stale results after archives, renames,
deletions, new chats, message edits, and watch-driven status changes.

## Fix

Reintroduces `invalidateChatSearches`, a prefix invalidation over the
module-private `chatSearchFamilyKey`, and wires it into:

- `chats.ts`: `archiveChat.onSettled`, `unarchiveChat.onSettled`,
`updateChatTitle.onSettled`, `editChatMessage.onSettled`,
`createChat.onSuccess`
- `useChatStore.ts`: `upsertCacheMessages` (unconditional; assistant
message bodies are indexed too) and `replaceCacheMessages`
- `AgentsPageLayout.tsx`: the `deleted` and root `created` watch
branches, the merge watch branch (gated by a new exported
`shouldInvalidateChatSearches` helper), the `has_unread` clearing
effect, the `onOpen` reconnect convergence, and
`archiveAndDeleteMutation.onSuccess`

The merge-branch gate only invalidates for search-affecting event kinds
(`title_change`, `status_change`, `diff_status_change`,
`action_required`). `summary_change`, `chat_summary_change`, and
`context_dirty` are excluded: stale `last_turn_summary` subtitles are
accepted until reconciliation lands.

## Backend constraint

Message bodies only enter full-text search via the dbpurge backfill
(`search_tsv` starts NULL and is populated every 10 minutes). Frontend
invalidation fixes removals, ordering, and rendered fields immediately,
but a chat that newly matches on message body will not appear until the
next backfill. This is a server-side eventual-consistency limit we
accept.

## Scope decisions (confirmed)

- No invalidation in `createChatMessage.onSuccess`: the send path
already routes through `useChatStore.upsertCacheMessages`; adding both
would double-invalidate every send.
- No invalidation for `pinChat`/`unpinChat`/`reorderPinnedChat`
(ordering-only, self-heals).
- ACL mutations out of scope.
- No coalescing/debouncing; that belongs to a later reconciler PR.

## Tests

- Prefix invalidation: multiple distinct `q` params invalidated,
bystanders (list, by-workspace, entity, messages, cost tree) untouched.
- Mutation wiring: settlement of `archiveChat`, `unarchiveChat`,
`updateChatTitle`, `editChatMessage`, and `createChat` invalidates a
seeded search key; `createChatMessage` asserted NOT to.
- `shouldInvalidateChatSearches` unit-tested over all
`ChatWatchEventKind` values.

PR generated by Coder Agents.
2026-08-06 10:25:36 +01:00
Danielle Maywood f96671d588 refactor(site/src): centralize chat cache operations in semantic cache functions (#27869) 2026-08-06 08:29:10 +01:00
Ethan 01c00ce1bf feat(site): add per-template Coder Agents controls (#27514)
Relates to CODAGT-713

Depends on #27285

This replaces the deployment-wide template allowlist UI with per-template Coder Agents controls. Each template's General Settings page exposes `agents_allowed` to organisation and template administrators through the existing template-update permission, whilst AI Settings > Templates gives deployment administrators a bulk management page. The bulk page requires both deployment-config and template-update permissions, reuses the same URL-backed server-side filter as `/templates`, tracks pending updates independently for each template, and surfaces failures through template-specific toasts. The copy refers to new workspace creation because disabling a template does not revoke an existing Coder Agent workspace.

This completes the frontend migration away from the experimental `/template-allowlist` API. #27515 removes the now-unused legacy storage, routes, SDK methods, generated types, and JSON utility. Later PRs add CLI flags and update the platform controls documentation. The stack is intended to merge as a unit.
2026-08-06 14:23:36 +10:00
Danielle Maywood 0c88c2accc refactor(site): migrate chats query keys to collections/entities taxonomy (#27841) 2026-08-05 21:19:41 +01:00
Jake Howell d23d0d5313 refactor(site): replace MUI update-check snackbar with <UpdateCheckNotice/> (#27728)
Replaces the MUI `Snackbar` / `Link` update-check toast in
`DashboardLayout` with a Tailwind `UpdateCheckNotice`, and adds
Storybook coverage for the layout and notice.

The notice stays declarative (not Sonner), offsets above the deployment
banner when the user can view deployment stats, and otherwise sits at
the normal bottom margin.

| Old | New |
| --- | --- |
| <img width="460" height="158" alt="UPDATE_NOTICE_OLD"
src="https://github.com/user-attachments/assets/d7ac9e5a-f69a-47de-89d5-9b6c6df4e1aa"
/> | <img width="440" height="157" alt="UPDATE_NOTICE_NEW"
src="https://github.com/user-attachments/assets/a5eee05b-0232-4668-bc3e-fd3ffdbad9b8"
/> |
2026-08-05 01:00:31 +10:00
Michael Suchacz 6b8f820493 feat: remove native chat cost tracking in favor of AI Gateway cost data (#27330)
## Stack Context

This stack makes AI Gateway data and budgets the source of truth for AI
spend controls.

1. Re-back the per-chat cost endpoint with AI Gateway data (#27328,
merged).
2. Remove native chat usage limits (#27329, merged).
3. **This PR, now based on `main`:** remove native chat cost tracking
and its dedicated admin UI.

## Summary

Removes native per-message price calculation, model pricing fields, cost
persistence, aggregate cost queries, and admin cost API types. It also
deletes the Analytics and Spend pages plus their legacy redirects. The
AI Gateway-backed per-chat cost row and compact budget indicators
remain.

The spend documentation is renamed to `spend-management.md` and updated
for the remaining surfaces, group budget APIs, CSV export, upgrade
handling for native pricing and cost history, and the absence of a
deployment-wide spend dashboard. The per-chat cost API documents that
data follows AI Gateway retention and reports zero after all matching
requests are purged.

No schema is dropped in this release. `chat_messages.total_cost_micros`
remains nullable and unwritten so replicas from the previous release can
continue inserting messages during rolling upgrades. #27600 tracks
removal after the compatibility window.

> Mux prepared this PR on Mike's behalf.
2026-08-04 12:27:38 +02:00
Michael Suchacz f0e6ac64b3 feat: remove native chat usage limits in favor of AI Gateway budgets (#27329)
## Stack Context

This stack makes AI Gateway data and budgets the source of truth for AI
spend controls.

1. Re-back the per-chat cost endpoint with AI Gateway data (#27328,
merged).
2. **This PR:** remove native chat usage limits.
3. Remove native chat cost tracking and its dedicated admin UI (#27330).

## Summary

Removes the native usage-limit API, SDK types, SQL, and chat enforcement
for deployment, user, and group chat limits. Compact AI Gateway budget
indicators remain in the Agents sidebar, user menu, and group settings.
Gateway budget rejections and provider quota failures continue to
classify as usage-limit errors, including a 409 response for synchronous
title generation.

Budget-period labels now use the API's UTC boundaries, so users see the
same dates in every browser timezone. The documentation explains the AI
Gateway replacement, its licensing requirements, and the differences
from native limits.

No schema is dropped in this release. The usage-limit table, index, user
and group columns, constraints, audit mappings, and generated scan
fields remain for mixed-version rolling upgrades. #27600 tracks their
removal after the compatibility window.

## Breaking change

Native day, week, and month chat spend limits are removed and are not
migrated. AI Gateway budgets are month-based, group-scoped with per-user
overrides, and require the AI Gateway entitlement. Deployments without
that entitlement no longer have chat spend enforcement.

> Mux prepared this PR on Mike's behalf.
2026-08-04 11:36:49 +02:00
Bobby HoandTracy Johnson 4245e4e378 feat: expose dynamic client registration in deployment settings (#27480)
Adds the admin-controlled OAuth2 Dynamic Client Registration setting
landed by #27316 (`GET`/`PUT /api/v2/oauth2-provider/settings`) to the
OAuth2 Applications deployment settings page, since it was previously
only reachable via the API or `coder oauth2-provider dcr
enable|disable`.

The page is now tabbed, **Applications** and **Settings**, so DCR has a
home that further OAuth2 settings can share (an Initial Access Token
setting is a likely next one). The active tab is backed by a `tab`
search param, so `?tab=settings` links straight to it, and an
unpermitted deep link falls back to **Applications** rather than
selecting nothing. On the Settings tab, DCR renders as a titled section
with a description, an `Enabled` badge when active, and an
Enable/Disable button.

Enabling opens a confirmation dialog, since it lets any OAuth2 client
self-register against the deployment without prior admin approval (RFC
7591). Disabling is immediate, no confirmation.

The control is a button rather than a switch on design feedback: a
switch reads as an immediate on/off flip, which conflicts with a
confirmation dialog standing in front of it, and it left the only
explanation of the risk inside a dialog that disappears. A button
carries the confirmation step without misrepresenting what a click
costs, the always-visible description explains the setting on the page,
and the `Enabled` badge gives the active state a persistent indicator.
The layout follows Tracy's mockup on `tj/oauth2-apps-pagination`; the
apps-table pagination work that shares that branch is deliberately not
included here.

Visibility and editability are gated on the same
`ResourceDeploymentConfig` RBAC checks the endpoint itself enforces
(`viewDeploymentConfig` / `editDeploymentConfig`), not a separate
hardcoded check. The view takes the settings values as one optional
`settings` prop, absent when the viewer lacks `viewDeploymentConfig`, so
"cannot view" is the shape of the prop rather than a flag the caller
keeps consistent with the values beside it, and the tab is not rendered
at all.

Closes https://github.com/coder/coder/issues/27432

## Where this sits in the request path

```mermaid
sequenceDiagram
    autonumber
    actor Admin
    participant View as OAuth2AppsSettingsPageView<br/>(Tabs + Enable/Disable + Dialog)
    participant Page as OAuth2AppsSettingsPage<br/>(React Query)
    participant S as coderd

    Note over Page: On mount
    Page->>S: GET /api/v2/oauth2-provider/settings
    S-->>Page: { dynamic_client_registration_enabled }
    Page-->>View: settings: { dynamicClientRegistrationEnabled, canEdit, ... }

    Note over Admin,View: Admin opens the Settings tab and enables DCR
    Admin->>View: click "Enable"
    View->>View: open confirmation dialog<br/>(no request sent yet)
    Admin->>View: click Confirm
    View->>Page: settings.onDynamicClientRegistrationChange(true)
    Page->>S: PUT /api/v2/oauth2-provider/settings<br/>{dynamic_client_registration_enabled: true}
    S-->>Page: 200 OK (audited)
    Page->>S: GET /api/v2/oauth2-provider/settings (refetch)
    S-->>Page: { dynamic_client_registration_enabled: true }
    Page-->>View: section shows the "Enabled" badge and a Disable button

    Note over Admin,View: Admin disables DCR
    Admin->>View: click "Disable"
    View->>Page: onDynamicClientRegistrationChange(false)<br/>(no dialog, disable is immediate)
    Page->>S: PUT ... {dynamic_client_registration_enabled: false}
    S-->>Page: 200 OK (audited)
```

## Files changed

All 10 files are hand-written; nothing in this PR is `make gen` output.

| File | What changed |
|---|---|
| `site/src/api/api.ts` | New
`getOAuth2ProviderSettings`/`putOAuth2ProviderSettings` methods, thin
typed wrappers around the two endpoints #27316 added to `main`. |
| `site/src/api/api.test.ts` | Covers both methods against the request
they issue and the error they propagate. |
| `site/src/api/queries/oauth2.ts` | A `getSettings` query and a
`putSettings` mutation that invalidates the settings key on success.
Both the app and settings keys now derive from a shared
`oauth2ProviderKey` constant. |
| `site/src/api/queries/oauth2.test.ts` | 4 tests: the key nesting, both
delegations, and that a successful update invalidates the settings key
without touching app queries. |
| `.../OAuth2AppsSettingsPage.tsx` | Wires query and mutation into the
page and passes the settings values down as one object, or omits it
entirely without `viewDeploymentConfig`. The apps error stays its own
prop, since the view gates the applications empty state on it. |
| `.../OAuth2AppsSettingsPageView.tsx` | `Tabs` splitting Applications
from Settings. The settings tab distinguishes loading, failed, and a
value the server omitted rather than rendering nothing, and the header's
"Add application" action is scoped to the applications tab. |
| `.../OAuth2AppsSettingsPageView.stories.tsx` | 14 stories, covering
the tab wiring, both permission boundaries, the header action's scope,
and the settings tab's loading, fetch-error, update-error, and
value-omitted states. |
| `.../DynamicClientRegistrationSetting.tsx` | The section itself:
heading, description including what disabling does not undo, `Enabled`
badge, a permission explanation when the viewer cannot edit, and one
button that confirms only in the enable direction. |
| `.../DynamicClientRegistrationSetting.stories.tsx` | 11 stories,
including focus surviving an in-flight request and the dialog ignoring a
value that changes underneath it. |
| `docs/admin/integrations/oauth2-provider.md` | Adds the web UI route
to the DCR section, which previously enumerated only the CLI and the
management API. |

## Suggested review order

Follows the direction data actually flows, from the raw HTTP call up to
the rendered section.

1. **`site/src/api/api.ts`**: the two new methods. Confirms they match
the `codersdk.OAuth2ProviderSettings` shape #27316 landed and sit next
to the existing OAuth2 app methods they mirror.
2. **`site/src/api/queries/oauth2.ts`**: the query/mutation pair. The
mutation's `onSuccess` → `invalidateQueries` is the one detail worth
double-checking: it's what makes the on-screen state catch up with what
was just saved, rather than trusting the PUT payload.
3. **`OAuth2AppsSettingsPage.tsx`**: the container. Check the two
separate permission gates (`viewDeploymentConfig` on the query's
`enabled` option, `editDeploymentConfig` on the button's editability)
match the RBAC the backend enforces.
4. **`OAuth2AppsSettingsPageView.tsx`**: the tabs and the settings tab's
four states. The `settings` prop being optional is what hides the tab;
the error inside the tab is deliberately separate from the page-level
`error`, which gates the applications empty state.
5. **`DynamicClientRegistrationSetting.tsx`**: the section. Two things
worth reading closely: the enable path opens the dialog while the
disable path calls straight through, and lacking permission uses the
native `disabled` attribute while an in-flight request uses
`aria-disabled`, so a keyboard user is not blurred mid-flip.
6. **The two story files**: read last, as they exercise everything above
without a real server. The dialog stories query
`canvasElement.ownerDocument.body` rather than `canvasElement`, since
the dialog renders into a portal attached to `<body>`.

## Deliberately not in this PR

- **ENG-3116**: the applications list cannot distinguish self-registered
clients from admin-created ones. Surfacing that needs a new field on
`codersdk.OAuth2ProviderApp`, which is an API addition this PR does not
need.
- **ENG-3118**: reusing the shared `EnabledBadge` and `SettingsHeader`
primitives for this section. Both hinge on what the mockup intends, and
the badge in particular is a visible change either here or on the four
other pages that share it.

## Screenshots

Default (disabled):
<img width="1676" height="497" alt="image"
src="https://github.com/user-attachments/assets/cfa60266-8678-410e-9577-16ef474491e3"
/>



Enabling (confirmation dialog):

<img width="1661" height="558" alt="image"
src="https://github.com/user-attachments/assets/a7d54fdd-f65d-4fec-9ed9-3bfdcfdae5be"
/>



Enabled:

<img width="1666" height="559" alt="image"
src="https://github.com/user-attachments/assets/d39251c2-771c-4608-81c2-dda151b35c3d"
/>

---------

Co-authored-by: Tracy Johnson <tracy@coder.com>
2026-08-03 08:29:35 -07:00
Michael Suchacz 95a2c2ba02 feat: back the per-chat cost endpoint with AI Gateway data (#27328)
## Stack Context

This stack removes native chat cost tracking and native chat usage
limits, making the AI Gateway the single source of AI spend data and
budget enforcement.

1. **This PR:** re-back the per-chat cost endpoint with AI Gateway data.
2. Remove native chat usage limits end to end, rewiring the sidebar
indicator to gateway spend.
3. Remove native chat cost tracking end to end, deleting the
Analytics/Spend cost UI.

## What?

`GET /api/experimental/chats/{chat}/cost` summed
`chat_messages.total_cost_micros`, which native chat cost tracking
maintained. It now aggregates AI Gateway interception data instead, and
has no native fallback.

- New `GetAIBridgeChatCost` query, authorized through the root chat so
members can read their own chat's cost without gaining access to raw
interception rows.
- Response fields renamed: `priced_message_count` -> `request_count`,
`unpriced_messages_having_usage_count` -> `unpriced_request_count`.
- The chat summary sidebar keys its cost cache by root chat, and hides
the cost row where the AI Gateway is off or unlicensed. The root cost is
invalidated when a chat leaves an active status and when a generated
title lands, since title generation bills its own gateway request.

`GetChatModelUsageCostByChatID` and the rest of native cost tracking are
untouched here; PR 3 removes them.

## Why?

Native cost tracking duplicates what the AI Gateway already records, and
the two disagree. Repointing the endpoint first means the cost UI keeps
working while the native implementation is deleted later in the stack.

Two behaviour changes follow from gateway semantics and are intentional:

- **Requests, not messages.** The gateway records interceptions, so
counts are requests. Title-generation traffic now counts.
- **Whole-tree totals.** The gateway records the *spawning* chat's ID as
the interception session ID, so a subagent's requests are attributed to
its immediate parent, not always the root. Only a whole chat tree can be
summed, so the query resolves the root and aggregates the tree, and
every chat in a tree reports the same total. Native returned per-subtree
totals.

## Attribution and counting semantics

The aggregate groups token usage per interception before counting, so
the reported numbers are per request even though a request records one
usage row per provider response:

- `RequestCount` counts finished `Coder Agents` interceptions in the
tree, including unpriced ones.
- `UnpricedRequestCount` counts requests with at least one usage row the
gateway could not price. It is a subset of `RequestCount`.
- `TotalCostMicros` omits only unpriced usage, so a partially priced
request still contributes its priced portion. The sidebar therefore says
`Excludes unpriced usage from N request(s)` rather than claiming whole
requests were dropped.

A recorded cost of zero is a free request, not an unpriced one. Usage
without an effective group is excluded, matching what never reached
`ai_user_daily_spend`.

## Authorization

Reads go through `ExtractChatParam` plus `ResourceChat`, with no
cost-specific RBAC widening. `TestGetChatCost/MemberCanReadOwnChat`
covers a scoped `agents-access` member reading their own chat's cost,
and `MemberCannotReadOtherUsersChat` still asserts 404 for a non-owner.
Plain members without `agents-access` cannot create or read chats at
all, so they never reach this endpoint.

## Known limitation

AI Gateway data has its own retention period, 60 days by default and
configured independently of chat retention, so spend for requests older
than that is no longer reported. A chat whose gateway records have all
been purged reports zero cost, which is indistinguishable from genuinely
free usage under this contract. The endpoint documents the caveat;
#27330 documents it on the Spend Management page.

In-flight interceptions are excluded, since cost is only known once the
response is recorded. A chat's cost therefore lags the active turn by
one request.

## Rebase note

Rebased onto `main` after #27579 removed the `ai-gateway-cost-control`
experiment. The per-chat cost row is now gated on the `aibridge` feature
alone, matching how #27579 degated the other cost-control surfaces.

> Mux prepared this PR on Mike's behalf.
2026-07-30 13:01:48 +02:00
Jake Howell 6c42309ccb feat(site): modernize OAuth2 applications settings UI (#27562)
Follow up to #27561 

Modernizes the Deployment Settings OAuth2 Applications create/edit/list
UI to match the AI Providers pattern: fat page views, card layout, and a
Formik + Yup form using shared field primitives.

- Rework CreateOAuth2AppPageView / EditOAuth2AppPageView into fat views
with provider-style layout (back link, avatar + title, bordered cards,
cancel/submit footer)
- Rewrite OAuth2AppForm with Formik/Yup, FormField descriptions, and
IconPickerField (live header avatar on create/edit)
- Order edit page as settings → endpoints (Client ID / Auth / Token via
CodeExample) → secrets
- Align list page row styling and add a Callback URL column
- Update Storybook stories for the new fat-view + form validation
behavior

| Old | New |
| --- | --- |
| <img width="2936" height="1802" alt="old-oauth2-application-create"
src="https://github.com/user-attachments/assets/98c1dec1-c273-43a7-a517-a31ae48bc17c"
/> | <img width="2936" height="1802" alt="new-oauth2-application-create"
src="https://github.com/user-attachments/assets/9d2e1d6b-6905-469f-b9e0-cf8ae3b14f3d"
/> |
| <img width="2936" height="1802" alt="old-oauth2-application-list"
src="https://github.com/user-attachments/assets/3d38daab-1d93-4acb-bff0-d7299da884fd"
/> | <img width="2936" height="1802" alt="new-oauth2-application-list"
src="https://github.com/user-attachments/assets/b3cc56ac-a810-4742-9ea7-27e20caa9bb7"
/> |
| <img width="2936" height="2030" alt="old-oauth2-application-update"
src="https://github.com/user-attachments/assets/544ad92b-ae2b-4b10-907d-c94bcc3d12c4"
/> | <img width="2936" height="3470" alt="new-oauth2-application-update"
src="https://github.com/user-attachments/assets/6e4d228d-8deb-4934-989a-b9c98b633509"
/> |
2026-07-30 02:46:09 +10:00
Michael Suchacz 4bf9b9d1e6 feat(site): surface chat lifecycle hook outcomes in the chats UI (#27430)
Surfaces chat lifecycle hook outcomes in the chats UI. Final PR of the
lifecycle hooks stack (#27401, #27428, #27429), all now merged.

- Show hook notices attached to their user message as timeline notes
(`role="note"` so historical notices stay out of the screen reader's
assertive live region), and show an info tooltip for notices on queued
messages.
- Cache the full inserted message batch from send and edit responses so
hook-inserted messages survive stream reconnects and queue promotion.
- Reconcile the promoted queue head after sending to an errored chat so
a missed or delayed queue update neither duplicates nor hides messages,
and clear the stale error status so the Thinking indicator appears
before the websocket status event.
- Ignore an authoritative queue snapshot that still contains a
just-promoted message: queued messages are delete-only, so such a
snapshot predates the promotion and would both re-show the promoted
message and drop messages queued since. Fresh snapshots apply in full
and clear the suppression.
- Cache the store's reconciled queue on `queue_update` instead of the
raw event, so a stale update cannot re-show a promoted message after
REST re-hydration.
- Refresh chat details when a send or edit fails, because a failed hook
dispatch can move the chat to the error state.
- Surface tool result error text in the tool rows: the execute failure
tooltip shows the actual error instead of a hardcoded "Command failed",
and a failed `write_file` renders an error label with the result error
text instead of "Wrote <file>" with an args-derived diff of content that
was never written. This makes hook tool denials legible in the timeline,
and benefits every failed execute or write.

- Label a tool call blocked by `pre_tool_use` as failed instead of `Ran
<command>`, matching what the write and edit tools already do. The
wording derives from the tool-result error flag, so a command that ran
and exited non-zero is unaffected.
- Render a hook notice below the message it annotates rather than above
it, which reads correctly for a "your prompt was rewritten" card.
- Give both hook outcomes their own treatment on the create path, where
they previously fell through to the generic error alert and an expected
policy decision appeared with a stack trace, response data, and a
workspaces action. Classification keys on the structured response body
rather than the status code, so ordinary permission errors keep their
existing rendering.

- Unrelated to the hooks work, de-flake `SchedulePage.test.tsx`. Its
`fillForm` helper wrapped an already-retrying `findByLabelText` in
`waitFor`, so the two 1s budgets raced and a slow first render failed
`test-js` with "Timed out in waitFor". This is separable from the rest
of the PR if you would rather it land on its own.

> This PR was written by Mux, an AI coding agent, on Mike's behalf.
2026-07-29 16:45:11 +02:00
Ehab Younes e3a5a697ab fix(site): refresh group member budgets after overrides (#27553)
Group member budget rows remained stale after saving or deleting a user override because the cached member-spend query was not refreshed.

Invalidate the affected user's override query and only cached group-member spend queries whose user ID list contains that user. This refreshes relevant rows without invalidating unrelated groups.
2026-07-29 17:09:15 +03: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
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
McKayla はな 00d134ebfd chore: remove classic parameter UI (#25014) 2026-07-27 19:02:28 -06: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
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
McKayla はな 3cf97ff8e7 fix: show selected owner's external auth when creating a workspace (#26653) 2026-07-23 16:39:53 -06: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
Michael Suchacz 02fd1cc691 feat: allow spawn_agent model and reasoning effort override (#27385) 2026-07-22 18:43:22 +02:00
Danielle Maywood 507ce504f8 refactor(site): align AgentsPage layout naming with codebase conventions (#27378) 2026-07-22 13:16:14 +01:00
Ehab Younes 6ec45f75c6 chore(site): replace handrolled AI spend types with generated types (#27342)
The groups and group members pages consumed AI spend through local ai_cost_control wrapper types with TODOs to adopt the generated contract. Both spend endpoints are now live, so this switches to the generated types and hardens the fetch path.

- Use OrganizationGroupsAISpend and GroupMembersAISpend from typesGenerated for the org groups and group members spend endpoints
- Fetch identity and spend separately, joining by group_id/user_id at the container so each row gets one enriched object
- Batch spend requests at the backend cap of 100 IDs in the API client and merge the responses, with table-driven tests covering both endpoints
- Treat a null effective_group_id as a budget managed by a group in another org: render an em-dash with an info tooltip and skip the group name lookup instead of firing an empty-ID request

Closes AIGOV-509
2026-07-22 12:17:22 +03:00
Michael Suchacz 3227cac217 feat: add manual chat compaction via /compact (#27081)
Adds a user-triggered `/compact` action for Coder Agents chats: typing
`/compact` in the composer (or picking it from the `/` trigger menu)
summarizes the conversation so far to free up context window space.

## How it works

- New `POST /api/experimental/chats/{chat}/compact` endpoint
(owner-only, RBAC `ActionUpdate`, excluded from the public API reference
via `x-apidocgen skip`). It marks the chat with a durable one-shot
`chats.compaction_requested_at` signal and moves it `waiting -> running`
via a new `RequestCompaction` state transition; no message row is
inserted. AI Gateway attribution needs no per-request key: generation
preparation resolves the owner's synthetic API key (#27170) like any
other turn.
- `RequestCompaction` hands off chat ownership (clears
`worker_id`/`runner_id`) so a worker acquisition hint is published;
since the transition changes no history, the previous runner could
otherwise miss the request under reordered pubsub delivery.
- The background chat worker picks the chat up like any other turn. A
pending manual request takes precedence over turn completion in the
generation decision, and forces compaction even below the automatic
threshold (and when compaction is disabled via threshold=100). The
commit step consumes the request marker in the same transaction; any
transition that ends the turn clears stale markers.
- The summary triplet reuses the automatic-compaction path, now tagged
with a `source` (`automatic` | `manual`) that is plumbed through
streamed progress parts, persisted tool JSON, and the UI label
("Summarized (manual)").
- Validation order: busy chats reject with 409 (state-machine conflict),
empty/already-compacted chats with 409 "nothing to compact", archived
chats with 400; the owner usage-limit check runs last so no-op requests
surface the specific conflict instead of a limit error.
- Web UI: the `/` trigger menu now has a built-in "Commands" group
listing `/compact`; submit intercepts exactly `/compact` and calls the
endpoint instead of sending a message. A personal or workspace skill
named `compact` takes precedence over the built-in command; while skill
collisions are still resolving, an exact `/compact` submission is
blocked with a retryable hint instead of leaking as message text.
History and queued-message edits are never intercepted. After
compaction, the context usage indicator resets to its unknown state
until the next assistant response reports fresh usage, instead of
showing the stale pre-compaction number.
- codersdk: `ExperimentalClient.CompactChat`.

Worker-path execution (rather than compacting synchronously in the
handler) reuses the existing lock fencing, live "Summarizing..."
streaming, retry accounting, restart resilience, and debug-run
observability. Rationale documented in `coderd/x/chatd/ARCHITECTURE.md`.

## Testing

- State machine: transition-matrix coverage for `RequestCompaction`,
marker lifecycle tests (carried by lease renewals/queue appends, cleared
by terminal transitions, consumed by commit), ownership handoff +
acquisition hint assertions.
- Worker: decision-ordering and forced-compaction unit tests;
active-server end-to-end test (manual compact below threshold produces a
`source=manual` summary, returns to `waiting`, no assistant follow-up;
busy chat rejected).
- API: success, archived, non-owner, RBAC-denied, empty-chat, no-daemon
cases; usage-limit ordering (at-limit owners still get
state/nothing-to-compact conflicts for no-op requests, with marker
rollback).
- Frontend: Storybook play tests for the Commands menu group, submit
intercept, skill-name collision, queued-edit passthrough, and
manual/automatic tool rendering; unit tests for command availability
resolution and the post-compaction context usage reset.

> This PR was created by Mux, an AI coding agent, working on Mike's
behalf.
2026-07-21 10:58:08 +02:00
Michael Suchacz 9b3af629cd fix: hide and reject chat models from disabled AI providers (#27070) 2026-07-19 08:33:40 +02:00
Michael Suchacz 46d1823c0a feat: add workspace skills to agent chat slash menu (#25600)
> This Pull Request was updated by Mux working on behalf of Mike.

Adds workspace skills to the agent chat slash menu, sourced entirely
from the chat's pinned context resources (the single-chat GET response
the page already fetches), the same inventory `read_skill` resolves
from. No new API endpoint is introduced.

Personal entries insert `/name`, or `/personal/name` when the name
collides with a workspace skill or the chat's pinned context has not
resolved yet; workspace entries insert `/workspace/name`. Qualified
aliases stay searchable even when the displayed trigger is bare. Before
a chat binds a workspace (new chat form, or a selected but unbound
workspace), the menu lists personal skills only.

Sending a message invalidates the chat detail query, and chatd
broadcasts a context watch event when a first-turn bind pins the chat,
so the menu picks up newly pinned context without a reload.

Makes `UpdateChatWorkspaceBinding` a no-op when the requested
workspace/build/agent binding is unchanged, preserving `updated_at` so
chat list ordering and watch events stay stable.

Includes regression coverage for the no-op binding guard, pinned-context
skill mapping, collision qualification, and skills menu behavior.

Refs
[CODAGT-474](https://linear.app/codercom/issue/CODAGT-474/ux-improvements-for-coder-agents)
(skills autocompleting in the editor).
2026-07-18 21:52:02 +02:00
Michael Suchacz 3dd9265fa6 feat: add UI option to disconnect OAuth2 MCP credentials (#27299)
Closes
[CODAGT-804](https://linear.app/codercom/issue/CODAGT-804/add-ui-option-to-revoke-oauth-mcp-credentials).

Users could authenticate with an OAuth2 MCP server from the chat input,
but there was no UI to disconnect those per-user credentials. The
backend endpoint (`DELETE
/api/experimental/mcp/servers/{id}/oauth2/disconnect`) already existed.

## Changes

- Connected OAuth2 MCP rows in the chat input plus menu now show a
disconnect icon button next to the enable switch. It opens a
confirmation dialog; confirming calls the disconnect endpoint, shows a
toast, and refetches MCP configs so the row reverts to the `Auth` button
without a reload.
- New `disconnectMCPServerOAuth2` API client method and react-query
mutation that invalidates `mcp-server-configs`.
- Storybook interaction tests: control visibility per auth state, cancel
makes no API call, confirm calls the endpoint once, failed disconnect
keeps the dialog open.
- Hardened `TestMCPServerConfigsOAuth2Disconnect`: seeded tokens flip
`auth_connected`, disconnect only removes the calling user's token, and
repeat disconnect stays idempotent.

The endpoint removes the token stored in Coder; it does not revoke the
upstream OAuth grant, so the UI copy says "disconnect" rather than
"revoke".

Validated with the targeted Go test, Storybook tests (51 passed), tsc,
biome, the react-compiler check, and a manual dogfood run (seeded token,
disconnect/cancel/reconnect flows verified in the UI).

> This PR was authored by Mux, an AI coding agent, on Mike's behalf.
2026-07-16 18:26:35 +02:00
Michael Suchacz 6f6d7539c8 feat: remove unused chat statuses pending, paused, and completed (#27064)
The chatd state machine only recognizes `waiting`, `running`, `error`,
`requires_action`, and `interrupting`. Remove the unused `pending`,
`paused`, and `completed` values from the database enum, backend, SDK,
frontend, generated queries, and API docs.

Migration `000543_chat_status_remove_unused` remaps existing `pending`
rows to `running`, remaps `paused` and `completed` rows to `waiting`,
drops the obsolete `idx_chats_pending` index, and recreates
`chats_expanded` around the enum swap. It also removes the dead
`AcquireChats` query and all remaining query literals for the deleted
statuses.

**NOTE**: The enum swap can break chat queries from older replicas
during a mixed-version rollout because they still reference
`'pending'::chat_status`. Chats are experimental, so this PR accepts
that limited rollout window instead of adding a two-release expand and
contract sequence.

> This PR was authored by Mux (AI agent) on Mike's behalf.
2026-07-13 20:28:28 +02:00
Ehab Younes d5e5b10ff1 feat(site): polish AI budget members table (#26805)
Finishing touches for the AI cost control group members table.

- Spend stays primary-colored until near the budget limit, via a new
  AIBudgetAmount component
- "AI budget period" label shows the current spend window in local
  time, next to the members tab
- Budget tooltip notes the reset date and the group's default limit
- "Budget type" renamed to "Budget group", with a badge for the
  governing group or override
- Distinguishes $0 budget ("None") from no budget ("Unlimited")
- Unattributed spend from another group shows a note instead of a dash
- "Manage AI budget" disabled only when another named group governs
- Replaces UserAISpend with generated UserAISpendStatus, fixing
  limit_source

Closes #26401
2026-07-13 17:33:40 +03:00
Danielle Maywood d51762440b feat: add custom AI provider icons and instance-based model picker grouping (#27026) 2026-07-06 23:00:09 +01:00
Jaayden HalkoandTracy Johnson 14d17abae6 chore: remove frontend related regenerate chat title code (#26867)
Co-authored-by: Tracy Johnson <tracy@coder.com>
2026-07-06 06:24:08 +01:00
Danielle Maywood 6b8c38b5a4 fix: gate chat advisor and virtual desktop behind experiments, delete experiments page (#26809) 2026-06-30 21:57:40 +01:00
Ehab Younes 6b297912a5 feat(site): add AI spend to user dropdown (#26698)
Adds the current user's AI spend progress to the top navbar avatar
dropdown when /api/v2/users/me/ai/spend reports a configured spend
limit. The shared dropdown content accepts an optional profileExtra
slot so the Agents sidebar can opt in later without changing the
default sidebar UI.

Introduces a temporary site API type and a React Query helper for
AIGOV-473 that refetches each time the dropdown opens, plus shared
budget progress helpers used by both the new dropdown bar and the
existing Agents usage indicator. The shared AIBudgetUsage component
moves to site/src/components so the dropdown and group budget UI
format spend identically, including the unlimited case. The avatar
border polls the spend endpoint and is colored by severity while the
dropdown is closed.

Closes AIGOV-473
2026-06-30 19:59:13 +03:00
Ehab Younes 75993c2ea0 feat(site): add group member AI budget columns (#26574)
Adds AI budget and Budget type columns to the group members table, shown when
aibridge is enabled and the ai-gateway-cost-control experiment is on. A member's
spend, limit, and source come from an ai_cost_control object embedded in the
group members and groups responses, so no extra request is made.

- Add AI budget and Budget type columns, gated by the aibridge feature and the
  ai-gateway-cost-control experiment
- Read ai_cost_control inline from the group and member lists instead of calling
  a separate spend endpoint
- Share an AIBudgetUsage component (spend vs budget with severity colors) and an
  InfoIconTooltip for the column headers
- When another group governs a member's budget, grey the spend and name that
  group in a tooltip; otherwise render the spend (severity-colored) against a
  white limit
- Resolve a member's effective group in the AI budget override dialog, marking
  only the governing group "(default)" and none when no group governs them
- Defer the override's custom-budget error until the field is touched

Closes AIGOV-291
2026-06-25 15:41:46 +03:00
Ehab Younes 0aa14c7d45 feat(site): add group AI budget column (#26566)
Adds an AI budget column to the organization Groups list showing each
group's current AI spend against its configured limit, or "unlimited"
when no limit is set.

- Column and its spend request are gated behind `aibridge` visibility
  and the `ai-gateway-cost-control` experiment
- Shows loading placeholders while spend is fetched from
  `/api/v2/organizations/{org}/groups/ai/spend`
- Spend severity thresholds extracted into shared `utils/budget.ts`:
  warning at 85%, destructive at or above the limit
- Response type defined locally with a TODO to replace with the
  generated type once the backend endpoint exists

Closes AIGOV-290
2026-06-23 18:56:54 +03:00
Jeremy Ruppel c9e94c20cc feat: TemplateCustomizationsStep and compose POST (DEVEX-287) (#26433)
Part of the Template Builder wizard PR stack.

## Frontend changes

1. **TemplateCustomizationsStep**: Final wizard step with org picker,
icon picker, name/display name/description/icon fields, and redirect on
success.

2. **Refactor**: Moved queries/mutation from PageView to Page container,
extracted `renderStepContent` switch and `computeCanContinue` switch
into standalone functions.

---
> [!NOTE]
> Generated by Coder Agents on behalf of @jeremyruppel
2026-06-23 10:23:00 -04:00
Jaayden Halko 77f1731d82 fix(site): stop chat mutations waiting on refetches (#26210)
closes CODAGT-571

Chat rename and archive actions previously kept user-visible loading
states active until sidebar and detail invalidations finished.

This patches accepted rename and archive state into loaded chat caches
immediately, removes archived or unarchived root chats from incompatible
sidebar filters, and leaves canonical chat list refreshes running in the
background.
2026-06-23 13:41:48 +01:00
Danielle Maywood ecfff8a7db feat: move model settings page to ai settings 2026-06-23 12:33:28 +01:00
Kyle Carberry e6a9b59abe feat(site/src): surface pinned chat workspace context in the UI (#26573)
Adds the workspace-context indicator UI for agent chats, part of
breaking the "Workspace Context Sources for Coder Agents" RFC (#26466)
into small, reviewable PRs.

## What this adds

- **Pinned context popover**: the context-usage indicator now lists the
chat's pinned resources, instruction files, skills, and MCP servers with
their tools. Unusable resources (invalid skill, unreadable/oversize
file) are surfaced in an "Issues" section with their error rather than
dropped silently.
- **Drift and error states**: when the pinned context differs from the
agent's latest snapshot, the ring shows a warning marker and the popover
explains the drift. A snapshot-level error gets a distinct treatment.
- **Refresh context**: a button re-pins the chat to the agent's latest
snapshot via `PUT /api/experimental/chats/{id}/context`.
- **Live updates**: `context_dirty` watch events apply the lightweight
dirty flags across the cached chat lists and refetch the open chat so
the full pinned detail loads.

## Not included

The context **changes/diff** view (the "View changes" affordance and its
dialog) is intentionally deferred to a later split, so this PR contains
no diff rendering.

## Testing

- `pnpm lint` (types, biome, circular deps, React Compiler, knip)
- `pnpm test src/api/queries/chats.test.ts` (cache-merge unit tests,
including the new `context_dirty` cases)
- `pnpm test:storybook
src/pages/AgentsPage/components/ContextUsageIndicator.stories.tsx` (4
stories)
- `pnpm format`

<details>
<summary>Design notes</summary>

This is **Split 3** of #26466. Split sequence:

1. #26558 - prompt pin consumption (merged)
2. #26570 - `codersdk` context resource types (merged). During review
the type was renamed `ChatContextMCPTool` -> `ChatContextTool` and the
field `mcp_tools` -> `tools`; this PR consumes those merged names.
3. **This PR** - the UI.
4. CLI `coder exp chat context` source CRUD + `refresh` (next).
5. The context diff (`changes`, `ChatContextResourceChange`, the changes
dialog, and `buildContentPatch`) - last.

Key decisions:

- The backend already publishes `ChatWatchEventKindContextDirty` and
exposes the refresh endpoint (#26389). Watch/pubsub payloads stay
lightweight: they carry only the `dirty`/`dirty_since`/`error` flags and
omit `resources`. So `mergeWatchedChatSummary` merges (not replaces) the
cached context to preserve the pinned `resources` a single-chat GET
populated, and the `AgentsPage` watch handler refetches only the open
chat to pull the full pinned detail.
- The indicator prefers the chat's pinned `resources`; while they have
not loaded it falls back to the agent's `last_injected_context`,
skipping the empty context-file placeholder so it never renders a
nameless row.
- All diff/changes rendering is excluded here and lands in the final
split to keep this PR focused on the read-only pinned view and the
refresh action.

</details>

*This PR was created by Coder Agents on behalf of @kylecarbs.*
2026-06-22 11:09:39 -06:00
Ehab Younes c0b8fa9418 feat(site): add user AI budget override UI (#26402)
Add a per-user AI budget override dialog in the group members menu, scoped to the group's organization.

Refs AIGOV-295
2026-06-22 13:58:53 +03:00
Jeremy RuppelandAndrew Aquino dca8178f8d feat(site): add ModuleSelectStep (#26427)
Implement the module selection wizard step with multi-select toggle and
conflict warnings.

- Add `getTemplateBuilderModules` API client method with optional `base`
parameter for OS filtering
- Add react-query wrapper with `staleTime: Infinity` to prevent
re-fetches on step navigation
- Render a flat grid of `ModuleCard` components with checkbox-style
multi-select
- Show non-blocking conflict warnings when selected modules declare
`conflicts_with` each other
- Map selections to `TemplateBuilderComposeModule[]` and
`SelectedModuleMeta[]` for wizard state

Relates to [DEVEX-285](https://linear.app/codercom/issue/DEVEX-285).

> [!NOTE]
> This PR was authored with Coder Agents.

---------

Co-authored-by: Andrew Aquino <dawneraq@gmail.com>
2026-06-18 09:42:03 -04:00
Jeremy Ruppel 286bd836f4 feat(site): add BaseInfraSelectStep (#26424)
Implement the first wizard step for selecting a base infrastructure
template.

- Add `getTemplateBuilderBases` API client method and react-query
wrapper
- Render a responsive grid of `TemplateCard` components fetched from
`GET /api/v2/templatebuilder/bases`
- Map `TemplateBuilderBase` to `SelectedBaseMeta` with `hasParameters`
derived from the base variables
- Single-choice selection that dispatches `SET_BASE` to wizard state,
preserving selection on back navigation

Relates to [DEVEX-283](https://linear.app/codercom/issue/DEVEX-283).

> [!NOTE]
> This PR was authored with Coder Agents.
2026-06-17 11:51:04 -04:00