mirror of
https://github.com/coder/coder.git
synced 2026-11-02 21:50:08 +08:00
3f3fd1c4d7c68768eb97384bfe2baa2392020d3e
4624
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3f3fd1c4d7 |
feat: show network request summary on AI session detail card (#27418)
Frontend for the AI session network summary. Adds Network calls, Blocked network requests, and Top domains rows to the Session summary card on the individual AI session detail page, driven by the network fields on the session threads response. Renders "Disabled" when network monitoring was not active and "No activity" when there were no calls. Covered by Storybook stories for each state. ### PR map (merge strictly bottom-up) This change is a 4-PR stack. Each PR depends on all the ones below it, so merge in this exact order: 1. #27417 — backend network summary 2. #27418 — frontend summary rows 3. #27425 — backend per-call list `network_call_logs` 4. #27426 — frontend network-calls panel Refs AIGOV-463 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Co-authored-by: Cian Johnston <cian@coder.com> |
||
|
|
841a1765f7 |
feat: add network calls summary to AI session threads API (#27417)
Backend for the AI session network summary. Exposes total/blocked
network calls and top destination domains on the session threads
endpoint (`GET /api/v2/ai-gateway/sessions/{id}`).
Total and blocked reuse the existing Agent Firewall aggregation from the
sessions list query, so the numbers match the sessions table. Top
domains are a new server-side aggregation
(`GetAIBridgeSessionTopDomains`) over boundary logs, using the same
interception-window correlation. There is no network-error state,
matching the current data model.
Frontend consuming these fields is in a separate stacked PR.
### PR map (merge strictly bottom-up)
This change is a 4-PR stack. Each PR depends on all the ones below it,
so merge in this exact order:
1. #27417 — backend network summary (base `main`)
2. #27418 — frontend summary rows (base #27417)
3. #27425 — backend per-call list `network_call_logs` (base #27418)
4. #27426 — frontend network-calls panel (base #27425)
Refs AIGOV-463
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Cian Johnston <cian@coder.com>
|
||
|
|
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.
|
||
|
|
2b28515d9b | refactor: migrate story snapshot params to pixel (#26844) | ||
|
|
740f5f7e1e |
chore: drop stale cost control experiment params (#27650)
Removes two stale `experiments: ["ai-gateway-cost-control"]` story parameters from `GroupPage.stories.tsx`. Refs #27579 #27553 |
||
|
|
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" /> | |
||
|
|
659fb48a1d |
fix: demui <OAuth2AppForm /> (#27561)
This pull-request removes the MUI styles from the `<OAuth2AppForm />` and adjacent components. |
||
|
|
0b93731ebf |
fix(site): reflect submitting state during batch update (#27630)
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
## Problem
When bulk updating workspaces, the confirmation modal's **Update**
button never entered a submitting/loading state, so there was no
feedback that the update was actually in progress.
## Root cause
The `BatchUpdateModalForm` shows a spinner when its `isProcessing` prop
is `true`. That prop is fed by `batchActions.isProcessing` from
`useBatchActions`. However, the `isProcessing` value was OR-ing together
every mutation's `isPending` flag **except** `updateAllMutation` — the
one that actually performs the batch update:
```ts
isProcessing:
favoriteAllMutation.isPending ||
unfavoriteAllMutation.isPending ||
startAllMutation.isPending ||
stopAllMutation.isPending ||
deleteAllMutation.isPending,
// updateAllMutation.isPending was missing
```
As a result, the button stayed idle for the entire duration of a bulk
update.
## Fix
Include `updateAllMutation.isPending` in the `isProcessing` derivation
so the button spinner and disabled state correctly reflect an in-flight
batch update.
## Testing
- [ ] Manually verify the Update button shows the spinner and is
disabled while a bulk update runs.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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._ |
||
|
|
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
|
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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 |
||
|
|
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
|
||
|
|
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._
|
||
|
|
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). |
||
|
|
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. |
||
|
|
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. |
||
|
|
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> |
||
|
|
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._ |
||
|
|
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. |
||
|
|
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" /> | |
||
|
|
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 🙂.
|
||
|
|
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. |
||
|
|
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
|
||
|
|
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. |
||
|
|
ea2ad4d6c6 |
chore(site): premium paywall for <WorkspaceProxyPage /> (#27567)
|
||
|
|
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.
|
||
|
|
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`). |
||
|
|
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> |
||
|
|
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. |
||
|
|
a3d67507e9 |
fix(site): demui <WorkspaceSettingsForm /> component (#27505)
This pull-request removes the imports of `@mui/material/*` from `<WorkspaceSettingsForm />`. |
||
|
|
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._ |
||
|
|
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. |
||
|
|
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.
|
||
|
|
00d134ebfd | chore: remove classic parameter UI (#25014) | ||
|
|
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" /> |
||
|
|
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> |
||
|
|
95be28850d | refactor(site/src/pages): delete unreachable tool render paths (#27527) | ||
|
|
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> |
||
|
|
60c20be46f | chore(site): remove beta labels from user secrets dashboard (#27512) | ||
|
|
fd2faaa2f8 | refactor(site/src/pages/AgentsPage): drop defensive code for impossible chat states (#27513) | ||
|
|
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> |
||
|
|
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.
|
||
|
|
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" /> | |
||
|
|
6120fb5988 | refactor(site): clean up workspace and template settings layouts (#25209) | ||
|
|
b8727d9c23 | fix: don't show admin settings dropdown to everyone (#27481) |