mirror of
https://github.com/coder/coder.git
synced 2026-10-31 19:36:41 +08:00
0ef331fcd0ca8e26c53f483aaf577a3f7aef9033
4829
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
0ef331fcd0 |
feat!: scope chat model override settings to organizations (#28442)
Moves chat model override settings from deployment scope to organization scope, following the org-scoping of chat model configs in #28440. Depends on #28440 (based on `ethan/chat-models-availability-consolidation`). Remote dogfood UAT passed on this change, including multi-organization scenarios on a licensed deployment with real models. ## Problem Chat model configs are now owned by organizations, but the override settings that reference them stayed deployment-wide: admin subagent/title/compaction overrides in `site_configs`, personal overrides in `user_configs`, and the advisor model embedded in the advisor runtime JSON. A single deployment-level override can only point at one organization's model config, so overrides effectively worked for the default organization only, and title generation had to soften its hard-failure contract to tolerate unusable overrides. ## Fix Admin overrides (general, explore, title generation, compaction, advisor) and personal overrides (root, general, explore) become organization-scoped rows in two new typed tables, `chat_organization_model_overrides` and `chat_user_model_overrides`. Composite foreign keys on `(organization_id, model_config_id)` make cross-organization model references unrepresentable. The migration deletes the legacy serialized `site_configs`/`user_configs` override keys instead of migrating them; admins and users re-select models in the new organization-scoped settings. New endpoints live under `/api/experimental/organizations/{organization}/chats/model-overrides` (admin defaults) and `/api/experimental/organizations/{organization}/members/{user}/chats/model-overrides` (personal overrides); the legacy deployment-level routes are removed. chatd resolves overrides via the chat's organization, restoring the hard-failure contract for configured-but-unusable title generation overrides. The deployment-level `AllowUsers` switch and advisor runtime limits stay deployment-scoped. The Coder Agents settings UI gains a per-organization "Defaults & overrides" page under AI Settings > Models, and the personal overrides page gains an organization selector for users in multiple organizations. Advisor telemetry now reports per-organization override rows. **BREAKING CHANGE**: Existing deployment-level overrides are dropped, not migrated. Every organization starts with no overrides after upgrade, and model selections must be re-applied in the new settings. The legacy deployment-level override endpoints are removed. Closes https://linear.app/coder/issue/CODAGT-872 > [!NOTE] > Xum (AI agent) authored this PR on behalf of @ibetitsmike. <!-- xum-attribution: model=claude-opus-4-6 thinking=high --> --------- Co-authored-by: Ethan Dickson <ethan@coder.com> |
||
|
|
3809314b31 |
refactor!: consolidate chat model availability (#28440)
## Summary Consolidate chat model management and runtime availability into a single organization model collection contract. The organization model response now includes provider availability, unavailable reasons, and unsupported-provider details, so callers no longer need a separate availability request. Depends on #28439. ## Problem The organization-scoped API exposes two overlapping representations of the same chat models: ```text GET /api/experimental/organizations/{organization}/chats/models GET /api/experimental/organizations/{organization}/chats/models/available ``` The collection route returns organization-owned model configurations and redacted provider descriptors. The availability route returns a second catalog-oriented response with provider availability and synthetic catalog entries. Callers must fetch, reconcile, and cache both responses even though they describe the same effective set of models. This creates duplicated server logic and separate SDK, OpenAPI, and frontend types. It can also produce inconsistent client state when one request succeeds while the other fails or when the two responses are refreshed at different times. ## Fix Return all model-management and runtime-availability information from the organization model collection route: ```text GET /api/experimental/organizations/{organization}/chats/models ``` `OrganizationChatModelsResponse` continues to return the caller-readable organization model configurations and redacted provider descriptors. Provider descriptors now also include: - `available` - `unavailable_reason` The response additionally includes `unsupported_providers`, allowing clients to explain configurations that the Agents harness cannot use. Frontend model settings, agent creation, existing chat recovery, and model override surfaces now consume this single collection query. This keeps model ownership, caller visibility, provider availability, and unsupported-provider guidance in one cache entry and one authorization path. The default-organization compatibility route remains available: ```text GET /api/experimental/chats/models ``` It now returns the same consolidated `OrganizationChatModelsResponse` contract instead of the former availability response. ## Breaking change This removes the separate organization model availability endpoint: ```text GET /api/experimental/organizations/{organization}/chats/models/available ``` Clients using that endpoint must migrate to: ```text GET /api/experimental/organizations/{organization}/chats/models ``` This PR also removes the corresponding public experimental client and schema surface: - `ExperimentalClient.ChatModelAvailability` - `ChatModelAvailabilityResponse` - `ChatModelProvider` - `ChatModelCatalogEntry` - The generated OpenAPI schema and frontend API/query types for the availability endpoint Callers must use `ExperimentalClient.ChatModels` and `OrganizationChatModelsResponse` instead. Availability is now reported on each `ChatModelProviderDescriptor`, and unsupported providers are returned in `OrganizationChatModelsResponse.UnsupportedProviders`. The default-organization compatibility route retains its URL, but its response schema changes from `ChatModelAvailabilityResponse` to `OrganizationChatModelsResponse`. Direct HTTP clients and generated clients that decode the previous response type must be updated even if they do not use the removed organization-scoped `/available` route. |
||
|
|
75ad45109d |
feat: use organization-scoped chat models (#27960)
Move chat model settings to `/ai/settings/organizations/:organization/models`, add an organization switcher, and redirect legacy settings paths. The pages use organization permissions for read, create, update, and delete controls. They use redacted provider descriptors. Agent creation loads models for the selected organization. Existing chats load models for the chat organization. Chat views warn about unavailable historical models, select a usable local recovery model, and disable generation when none exists. Settings that aggregate organizations retain successful results when one request fails. Depends on #27959 _This pull request description was generated by Coder Agents._ |
||
|
|
aefc761fcf |
feat!: enforce organization scope for chat models (#27959)
## Summary Move chat model management and runtime availability to organization-scoped API routes. The API now identifies the organization in every model-management operation, applies chat model RBAC and ACLs within that organization, and returns only models the caller can use there. Depends on #27958. ## Problem Chat model configurations now belong to organizations, but the existing experimental management and discovery APIs are deployment-shaped. Their routes do not identify an organization, SDK methods do not accept an organization ID, and item operations address a model configuration by ID alone. That contract cannot safely expose organization-owned model configurations. It also does not provide the organization-specific provider information, authorization behavior, ACL management, or recovery guidance needed by the settings and chat clients later in the stack. ## Fix Add organization-scoped chat model endpoints under: ```text /api/experimental/organizations/{organization}/chats/models ``` The new API provides: - Organization-scoped model listing and creation. - Model get, update, and delete operations scoped by both organization and model ID. - Model ACL read and update operations. - Organization-scoped runtime availability and provider status. - Redacted provider descriptors for model-management clients. - Chat model RBAC and row ACL enforcement for writes, runtime discovery, and Chatd model selection. - Recovery guidance when a chat's organization has no usable local model. Historical chats can retain a model reference owned by another organization, but runtime selection uses an authorized local default when one is available. Foreign, inaccessible, disabled, and otherwise unusable models are not exposed as usable selections for the organization. The existing collection and availability routes remain as default-organization compatibility endpoints in this layer: ```text GET /api/experimental/chats/model-configs POST /api/experimental/chats/model-configs GET /api/experimental/chats/models ``` These compatibility routes do not provide access to models in non-default organizations. The previous unscoped item update and delete routes are removed because item operations must include the owning organization. ## Breaking change This changes the experimental HTTP API and Go SDK contracts for chat model management. Clients must migrate model-management and availability requests to the organization-scoped routes and provide an organization identifier. In the Go SDK, the following methods now require an organization ID: - `ChatModelAvailability` - `ChatModels` - `CreateChatModel` - `UpdateChatModel` - `DeleteChatModel` `ChatModels` also changes its return value from `[]ChatModel` to `OrganizationChatModelsResponse`, which includes the organization models and the redacted provider descriptors required by management clients. New item and ACL methods likewise require both the organization ID and model ID. The default-organization compatibility routes reduce the immediate impact for collection and availability callers, but they do not preserve the previous item API or SDK method signatures. Generated clients and direct consumers of the experimental API must be updated before adopting this change. _This pull request description was generated by Coder Agents._ |
||
|
|
9bd8899359 |
refactor: clarify chat model resource names (#27958)
Rename the admin-managed SDK resource from `ChatModelConfig` to `ChatModel`. Rename catalog entries from `ChatModel` to `ChatModelCatalogEntry` and replace `ChatModelsResponse` with `ChatModelAvailabilityResponse`. API routes and JSON fields do not change. Experimental SDK and OpenAPI schema names do change. Depends on #27957 _This pull request description was generated by Coder Agents._ |
||
|
|
cbe24a3da5 |
feat: add chat model config RBAC resource (#27957)
Add the `chat_model_config` RBAC resource, actions, and API key scopes. This layer includes the scope migration and generated API types. Model list queries apply token scopes, SQL authorization filters, and row ACLs. Write and default-management handlers keep deployment configuration authorization until the next API layer replaces it. Depends on #27956 _This pull request description was generated by Coder Agents._ |
||
|
|
c9a1e2bcee |
fix(site): update Coder Agents license copy (#28432)
Updates the Coder Agents license card to consistently refer to concurrent agents, removes the usage management link, and links to the licensing and usage documentation. Updates Storybook coverage for the revised labels, tooltip, and link behavior. <details> <summary>Coder Agents disclosure</summary> This pull request was generated by Coder Agents on behalf of @jaaydenh. </details> |
||
|
|
610f321d52 |
fix(site/src): disable archive actions for active chats (#28264)
Closes [CODAGT-632](https://linear.app/codercom/issue/CODAGT-632/gray-out-the-archive-chat-button-when-chat-is-running). The chatd state machine only permits archiving a chat family from idle execution states (`waiting`/`error`); archiving an active chat fails with HTTP 409. The agents UI still offered "Archive agent" and "Archive & delete workspace" for active chats, and `AgentsPageLayout` showed a confirm dialog claiming "Archiving it will interrupt the current run", which is no longer true: confirming just surfaced the 409 error toast. ## Changes - `ChatActionsMenuItems` gains a `chatFamilyAllowsArchive` helper mirroring the backend rule: archive is allowed only when the root chat and every embedded child are `waiting`/`error` (archive cascades atomically over the family, so an active child also rejects it). Unknown status stays fail-open with the server 409 as backstop. A new `isArchiveBlocked` prop disables both archive items; unarchive, pin, and rename are unaffected. - While blocked, the menu shows a visible hint ("Interrupt or wait for the agent to finish first.") and both disabled items reference it via `aria-describedby`. - Sidebar rows (`ChatTreeNode`, covering the dropdown and the right-click context menu) gate on the chat list record, which watch events already keep fresh. The chat top bar gates on the live stream status (`liveChatStatus`) plus the same list record's children, passed through the layout outlet context, so all three menus read one live source and re-evaluate without a reload. - Removed the stale "Archive agent?" interrupt-confirm dialog and its pending-state plumbing from `AgentsPageLayout`; archive now mutates directly. - "Archive & delete workspace" is now archive-first: the archive call is the atomic server-side eligibility check and completes before the destructive delete, so a chat that becomes active mid-flow can never lose its workspace. On delete failure the chat deliberately stays archived, with no compensating unarchive: the delete outcome can be ambiguous client-side (a late 5xx can arrive after the delete build committed), so restoring the chat could resurrect it while its workspace is being deleted. Instead the mutation refetches all chat collections on any failure and the toast directs the user to the archived filter, where Unarchive is one click. This is a deliberate departure from the self-restoring retry surface of #26900. The workspace-deletion confirm dialog is unchanged. ## Scope The client gate is best-effort UX; the server-side 409 is the correctness backstop. Rare staleness (a chat outside the loaded sidebar pages, events missed across a socket reconnect) leaves the actions enabled and the server rejects the request with the existing error toast. Cache-coherency hardening for those windows was deliberately removed to keep the change proportionate to the defect. ## Testing - Storybook play stories: disabled archive items with the accessible hint for an active chat and for an idle parent with a running child; enabled items for an idle chat; in the top bar menu and the sidebar dropdown/context menu (`ChatTopBar.stories.tsx`, `ChatsSidebar.stories.tsx`). - Unit tests: archive-first ordering, delete failures keep the chat archived and rethrow (`agentWorkspaceUtils.test.ts`). - `pnpm -C site check`, `pnpm -C site lint:types`, whole-file vitest runs for every touched test/story file. - Remote dogfood UAT on a deployment built from this branch: archive items disabled while running and in `requires_action`, re-enabled live on transition to `waiting`, archive works from `waiting` and `error` with no confirm dialog, unarchive unaffected. > Shux acted on Mike's behalf to create this pull request. |
||
|
|
7b72f5d86b |
fix(site/src/pages): keep TemplateCard official badge inline under preflight (#28433)
## Summary In the Template Builder base template select grid, each base template card's "official" checkmark badge was rendering below the card title (`h3`) instead of inline and vertically centered with it. ## Root cause Tailwind preflight was enabled in #27821 (removed `corePlugins: { preflight: false }`), which applies `svg { display: block }`. The badge relied on `align-middle`, which is a no-op on block-level elements, so the icon dropped onto its own line. The same badge in `ModuleCard` was patched in #27821 with `inline-block`, but the identical badge in `TemplateCard` was missed. ## Fix - `TemplateCard.tsx`: add `inline-block` to the badge so it flows inline within the title's text (trailing the last word and wrapping naturally), with `align-middle` handling vertical centering. This matches the existing `ModuleCard` pattern. - `Response.tsx`: update a stale comment that referenced preflight being disabled. ## Verification - `biome check` passes on both files. - Confirmed via a repo-wide sweep that no other inline icon layouts regressed from the preflight change (all other icon-next-to-text usages are protected by flex/inline-flex containers, `asChild` class merging, or are sole children). <img width="1083" height="626" alt="Screenshot 2026-08-21 at 12 02 47 PM" src="https://github.com/user-attachments/assets/2183dbf4-1f4f-4aae-92cf-21c11efebc5e" /> <details> <summary>Investigation notes</summary> Timeline: - Preflight was explicitly disabled via `corePlugins: { preflight: false }`. - The official badge (#26806, #27036, #27797) worked because the lucide SVG rendered inline while preflight was off. - #27821 ("refactor: remove MUI and Emotion") removed the `corePlugins.preflight: false` block, enabling preflight and its `svg { display: block }` rule. It patched `ModuleCard` with `inline-block` but missed `TemplateCard`. A read-only sweep across `site/src` found no additional broken inline-icon layouts. Candidates like `DERPRegionPage` (icon inside an `inline-flex` `Link`) and `TemplateCustomizationsStep` `BaseTemplateCard` (intended vertical layout) are safe. </details> --- _This PR was generated by Coder Agents on behalf of @jeremyruppel._ |
||
|
|
ee87ad6521 |
fix(site): point queued-capacity trial link to /deployment/premium (#28437)
Points the "Start an unlimited trial" link in the concurrency-limit queued callout (`QueuedForCapacityCallout`) to the internal `/deployment/premium` route instead of the external `https://coder.com/trial` URL, matching the convention used elsewhere in the app (e.g. `LicensesSettingsPageView`, `Paywall`). Uses `Link asChild` + `RouterLink` so navigation is client-side (SPA) instead of a full page reload. Also fixes all four links in the callout (`Start an unlimited trial`, both `learn more` links, and the `sales@coder.com` mailto) rendering at 12px (`size="sm"`) while the surrounding message text is 14px. Dropping the explicit `size="sm"` lets the links fall back to `Link`'s default size, matching `AlertDescription`'s `text-sm` (14px). <details> <summary>Decision log</summary> - Confirmed `/deployment/premium` is an existing internal route (`PREMIUM_PAGE_PATH` constant in `Paywall.tsx`), not an external link. - Existing pattern for internal links elsewhere in the codebase is `<Link asChild showExternalIcon={false}><RouterLink to="..." /></Link>`, so this change follows that convention rather than using a plain `<a href>`. - Only the admin-on-Community-license path (`canManageLicenses && !hasLicense`) is affected by the route change; the member and paid-license admin messages are unchanged. - Verified in a local `develop.sh` dev instance: HMR picked up the change cleanly, TypeScript reports 0 errors, and the links now render at 14px matching the message body. </details> --- PR generated with Coder Agents. |
||
|
|
cb52f5339d | feat: show a premium paywall on the external auth settings page (#28435) | ||
|
|
9ce42293b0 | fix(site/src): use the browser-only bullets from the copy doc (#28434) | ||
|
|
b8fcdbd32f | fix(site/src): sync paywall copy with the latest copy doc (#28436) | ||
|
|
a2afb458c5 |
feat: add premium paywall conversion telemetry (#28425)
**TLDR:** When someone using free Coder taps a "Start trial for free" button, we now write down which page they tapped it on, and whether they went on to actually start the trial. That tells us which locked features make people want Premium. ## Brief summary - Records two things: the button being clicked, and a trial actually being started. - Each click gets a random ID that travels with the person to the trial form, so we can tell who finished from who walked away. - Records a short label for the page (like `appearance` or `audit_log`), never the web address, so no customer names are collected. #28226 added the trial form and has since merged, so this now targets `main`. ## Description Today the "Start trial for free" button just moves you to the Premium page and nothing is recorded, so there is no way to know which feature convinced someone. This adds two events: | Event | Happens when | |---|---| | `cta_click` | Someone clicks "Start trial for free" | | `trial_signup` | A trial licence is actually issued | The click generates a random ID. It rides along to the trial form and gets saved on the signup, so a click and a signup can be matched up later. Clicks with no matching signup are the people who gave up. Trials started without a paywall (for example straight from the sidebar) are labelled `direct`, so "no label" never gets confused with "broken". A few small choices worth knowing: - The ID is kept in browser session storage, not in the web address, so the URL stays clean. It expires after 30 minutes. - The trial signup event is recorded by the server, not the browser, so nobody can fake a signup. - The browser cannot say which kind of event it is sending. It can only report a click; the server labels it. - Only people allowed to start a trial are counted. The server rejects the rest, which matters because a few paywalls show their button to everyone. - Nothing is recorded at all when telemetry is turned off. There is deliberately no "paywall was shown" event. Every admin page visit would report one, which is a lot of noise for little insight. Every paywall in the [Premium Copy Updates](https://www.notion.so/coderhq/Premium-Copy-Updates-3c1d579be59280a59557cabd54fd8a1f) table has a label. The rows that do not are badges and alerts with no button to click. ## Proof Ran a local Coder with no licence and clicked the paywall button on the Appearance page. What the browser sent, and the reply: ```json POST /api/v2/deployment/premium-funnel-events -> 204 No Content {"id":"92d96afa-f975-4789-b8a7-b1fd19d4df85","source":"appearance","variant":"premium"} ``` What the browser saved for the trial form, same ID: ```json {"id":"92d96afa-f975-4789-b8a7-b1fd19d4df85","source":"appearance","createdAt":1787268520857} ``` The click then landed on the Premium page with the trial form. Screenshots of the network panel, the saved value, and the Premium page are attached below. The form itself was not submitted, since that would contact the real licence server. ## Testing - Backend tests: a click is recorded with the server's own label, an unknown page label is refused, and a normal member is refused. - Storybook tests: button clicked and ID saved on all three paywall styles, plus the case where the viewer only sees guidance text and has nothing to click. - Unit tests for the saved ID: expiry, bad data, unknown page label, and the `direct` fallback. - `make gen` is clean, and lint, types, and formatting pass. Two stories fail on `main` today and fail the same way here: `ExternalAuthSettingsPageView > Page` and `PremiumPageView > No License`. Neither file is touched by this branch; both were verified failing on `main` with this branch checked out elsewhere. ## Follow-up coder/coder-telemetry-server#45 saves these events for reporting. It merges after this one. <details> <summary>Plan and decisions</summary> **Goal:** find out which Premium paywall drives trial signups, and tell abandons apart from completions. **Options considered and dropped:** 1. Put `?from=appearance` in the URL. Rejected: messy for users. 2. Use router state. Rejected: lost if the page is refreshed. 3. Let the browser report the signup. Rejected: it could be faked or lost during the redirect. 4. Work out the page from the URL. Rejected: URLs contain organization and template names. 5. Report every paywall impression. Rejected: one event per admin page visit is noise, and the click is the signal. 6. Separate event types and tables. Rejected: makes the matching query harder for no gain. **Event shape:** ```go type PremiumFunnelEvent struct { ID uuid.UUID // event ID; for a click this is also the matching ID EventType string // cta_click | trial_signup, set by the server Source string // appearance, audit_log, ..., direct Variant string // which paywall design was shown AttributionID uuid.UUID // the click this signup came from UserID uuid.UUID CreatedAt time.Time } ``` **Where the code lives:** nothing under `components/` imports from `modules/`, so the paywall components stay presentational and only gain an `onCTAClick` prop. New wrappers in `modules/paywall/` own the reporting, one per paywall style, each taking a fixed `source`. **Self-review:** the `frontend-review` skill caught that reading permissions inside the wrapper forced an auth context into presentational page views, which broke their stories. Every call site already passes `canViewPremium={permissions.viewAllLicenses}` and the button only renders when that is true, so the wrapper no longer reads permissions and the server stays the gate. It also flagged one unattributed paywall on the Security page, now wired up as `browser_only`. </details> > Generated with [Coder Agents](https://coder.com/agents) on behalf of @david-fraley |
||
|
|
f44a3b59d7 |
feat: Premium Page CTA updates and form handling (#28226)
* Premium page has form for signing up for a trial * Input validation and normalization for trial form fields * api endpoint definition to make this processes seamless * license page updates to access `?success=true` queryparam and confetti <img width="1512" height="833" alt="Screenshot 2026-08-17 at 10 08 41 PM" src="https://github.com/user-attachments/assets/1766e724-c63a-4da9-b2bd-a98e6b66cc9b" /> <img width="1510" height="874" alt="Aug-17-2026 22-12-38" src="https://github.com/user-attachments/assets/3a2a0f35-1176-4290-aa0d-b3f7d66c15d6" /> ```mermaid sequenceDiagram autonumber actor Owner participant UI as UI<br/>TrialRequestForm + PremiumPage participant API as API<br/>api.ts + coderd middleware participant Coderd as Coderd<br/>postTrialLicense + trialer participant Licensor as Licensor<br/>v2-licensor + Postgres + pubsub + cache Note over UI: mutation.status = "idle" Owner->>UI: fills 10 fields, checks acknowledgement UI->>UI: Yup validate, strip "acknowledged", submit UI->>API: POST /api/v2/licenses/trial<br/>cookie + X-CSRF-TOKEN Note over UI: mutation.status = "pending" API->>API: verify CSRF, apiRateLimiter,<br/>apiKeyMiddleware sets actor API->>Coderd: dispatch Coderd->>Coderd: audit.InitRequest, authorize license:create,<br/>httpapi.Read validates, HasLicense is false Coderd->>Licensor: POST trial request<br/>deployment_id unspoofable, 10s deadline Licensor-->>Coderd: 200, raw signed JWT Coderd->>Coderd: ParseClaimsIgnoreNbf, uuid.Parse Coderd->>Licensor: InsertLicense, updateEntitlements,<br/>publish PubsubEventLicense, commit audit log Licensor-->>Coderd: database.License Coderd-->>API: 201 codersdk.License API-->>UI: 201 codersdk.License Note over UI: mutation.status = "success" UI->>Licensor: invalidate entitlements + licenses keys UI->>Owner: navigate /deployment/premium?success=true Note over Owner: confetti fires on the success param ``` --------- Co-authored-by: Matt Vollmer <matthewjvollmer@outlook.com> |
||
|
|
58c1248cf3 |
fix(site/src): repair failing storybook interaction tests and pixel captures (#28417)
The Storybook suites fail on main in two independent ways: three interaction tests in the locally-run Vitest suite (`pnpm vitest run --project=storybook`, which does not run in CI), and three story captures in CI's pixel snapshot job, whose failures do not fail the GitHub check (the job PATCHes `status=failed` to pixel.coder.com and still exits 0). Vitest interaction failures: * `ModelForm > Cost Estimate Shows Loading While Debounce Pending` has been broken since it landed in #28298. In add mode the model identifier field is the Known Model autocomplete, which only commits typed text to `form.values.model` on option selection or dropdown close, so typing free text left the form's model empty and the pricing section never entered its loading state. The story now selects the catalog option. This also fixes the same story's capture failure in the pixel job. * The `SecretsPageView` import error stories raced the dialog enter animation: `findByText` resolves on DOM presence, and under parallel-suite CPU load `toBeVisible()` can run while the Radix dialog content is still at `opacity: 0` from its `fade-in-0` animation. These now poll with `waitFor(...toBeVisible())`, matching the file's other import stories. Pixel capture failures: * `NavbarView > ForMCPCreateOnlyAdmin` / `ForMCPDeleteOnlyAdmin` inherit the meta's tablet+desktop matrix but click the "Admin settings" button that only exists in the desktop navbar, so their tablet captures always fail. They now pin `pixelWithDesktop` like every other admin story in the file. * `AgentsPageLayout > OrganizationScopedMCPServers` fails intermittently because `CompactOrgSelector` option names are timing-dependent: when an org icon fails to load (as in the pixel static server), the avatar fallback initials render and join the option's accessible name (`"MY My Organization 2"`), breaking exact-name queries and polluting screen-reader output. The decorative avatar is now `aria-hidden`, which was verified red-green by forcing unloadable icons locally. > [!NOTE] > Shux (AI agent) authored this PR on behalf of @ibetitsmike. <!-- shux-attribution: model=claude-opus-4-6 thinking=high --> |
||
|
|
05fcd06979 | fix(site): auto-select MCP server after successful OAuth (#28155) | ||
|
|
fd09112e28 |
fix(site): right-align DateTimeRangePicker dropdown to match other dropdowns (#28428)
Follow-up to #28392. The `DateTimeRangePicker` popover opened with `align="start"` (left-aligned to the trigger), while the other dropdowns on the page use `align="end"`. This changes the `PopoverContent` alignment to `align="end"` for consistency. One-line change; no behavior or layout changes inside the dropdown itself. --- Generated by Coder Agents on behalf of @tracyjohnsonux. |
||
|
|
3ffb266af6 |
fix(site): align settings header style and paywall spacing (#28418)
Settings pages rendered the premium upsell banner at different vertical
offsets and the IdP sync pages used a lighter heading style than the
rest of the settings UI.
Three causes:
1. The IdP Sync (`/organizations/:org/idp-sync`) and Organization IdP
Sync (`/deployment/idp-org-sync`) pages hand-rolled their headers with a
bare `<h1 className="text-3xl m-0">` (missing `font-semibold`) and a
`flex flex-col gap-12` wrapper (48px above the banner).
2. Workspace Proxies and Appearance wrapped `SettingsHeader` in flex
columns with their own `gap` (`gap-4`, `gap-8`), which stacked on top of
the header's built-in `pb-6`, pushing the banner to 40px and 56px
offsets respectively while Groups sits at 24px.
3. `SettingsHeaderDescription` used `leading-normal` (21px lines) while
the inline `Link` component ("View docs") renders a 24px-tall
`inline-flex leading-6` box, inflating the line box on pages whose
description contains a docs link and shifting content below by ~3px.
Changes:
- Replace the custom header markup on both IdP sync pages with the
shared `SettingsHeader`, `SettingsHeaderTitle`, and
`SettingsHeaderDescription` components; the Export Policy button moves
into the `actions` slot.
- Hoist `SettingsHeader` out of the gap wrappers on Workspace Proxies
and Appearance so the header's `pb-6` is the single source of
header-to-content spacing; the gap now applies only within the entitled
content.
- Set `SettingsHeaderDescription` to `leading-6` so description line
height matches the inline `Link`, whether or not a docs link is present.
All affected pages now start content, including the premium paywall,
24px below the header description.
_Created by Coder Agents on behalf of @tracyjohnsonux._
|
||
|
|
1c139402d0 |
feat(site/src): use DateTimeRangePicker on the AI sessions page (#28426)
Follow-up to #28392. Wires the new `DateTimeRangePicker` into the AI Gateway sessions page, replacing the text-expression `DateTimeRangeFilter`, and removes the redundant "Filters" preset button. - The picker trigger sits in the filter row alongside the search field and the User/Provider/Client/Model dropdowns. - The preset queries (All sessions, My sessions) behind the Filters button duplicate the search field and the user dropdown, so the button is dropped by passing no presets. Other pages using the shared `Filter` component are unaffected. - No API or URL contract changes: the filter query still stores resolved `started_after`/`started_before` timestamps. The picker's preset id is display-only component state, shown only while the URL range still matches what the preset resolved to, so shared links keep rendering as custom ranges. - Clearing the search resets the picker to the default window, labeled "Last 24 hours". - The picker trigger chevron now uses the shared animated `ChevronDownIcon`, rotating on open like the other dropdown triggers. - The old `DateTimeRangeFilter` component is left in place (still referenced by its stories); removing it can be a separate cleanup. Storybook: the sessions filter story now asserts the Filters button is gone and exercises opening the picker; existing stories updated to the new value shape. `biome`, `tsc`, and the Storybook interaction tests for the page pass. --- Generated by Coder Agents on behalf of @tracyjohnsonux. |
||
|
|
af05849c9c |
feat(site/src/components): add date-time range picker with quick picks (#28392)
Adds a frontend-only `DateTimeRangePicker` component: a date range
picker with quick picks (Last 15 min, Last hour, Today, This week) and
per-boundary time selection. The dropdown opens as just the quick-pick
list; the calendar and From/To time fields only appear after selecting
"Custom range". Quick picks commit immediately, custom ranges commit via
Apply.
No backend changes. The value is always resolved dates:
```ts
interface DateTimeRangeValue {
start: Date;
end: Date;
preset?: string; // display metadata only, never sent to the backend
}
```
Quick picks resolve to concrete `start`/`end` at selection time;
`preset` exists solely so the trigger can keep rendering the preset
label. Consumers send `start`/`end` to the API in UTC via
`Date.toISOString()`.
Composed from existing primitives (`Calendar`, `Popover`, `Select`,
`Input`, `Button`); the `Calendar` component is unchanged. Includes 9
Storybook stories with play-function coverage: quick-picks-only open
state, immediate preset commit, custom panel expansion, apply flow,
invalid time validation, end-before-start validation, and cancel
discarding the draft.
<details>
<summary>Design notes and follow-ups</summary>
## Mockup deviations
- Added an Apply/Cancel footer to the custom panel. The design mockup
does not show one, but a draft range needs an explicit commit point.
- Trigger uses the shared `Button` outline variant to match the design
system.
## Settled decisions
- The API receives resolved timestamps, not preset tokens.
- All values sent to the backend are UTC; the component works in
browser-local time and conversion happens at the API boundary.
## Follow-ups (separate PRs)
- Wire the component into the pages that need it, with query-param
persistence where ranges should be shareable.
- The existing `DateTimeRangeFilter` (text-expression based, used by the
AIBridge sessions filter) is a candidate for migration to this design.
- Possible polish per design feedback: masked/segmented time input,
optional seconds, 24-hour locale support.
</details>
---
Generated by Coder Agents on behalf of @tracyjohnsonux.
|
||
|
|
e7eea0f06f | feat: update premium paywall copy and link each page to its feature docs (#28339) | ||
|
|
cfc7b825c2 |
fix(site/src/components/MultiSelectCombobox): match Input height when empty (#28409)
The empty `MultiSelectCombobox` rendered ~46px tall next to the fixed
40px `Input`, breaking form rows that place them side by side, most
visibly the IdP sync mapping fields (`/organizations/{org}/idp-sync`,
IdP org sync, template/parameter forms).
## Root cause
#27821 removed MUI's `CssBaseline` and enabled Tailwind preflight
(`corePlugins.preflight` was previously `false`). Preflight sets `input
{ line-height: inherit }`, so the combobox's inner cmdk input started
inheriting the wrapper's `text-sm` 24px line instead of the browser
default. With its `py-2.5` padding the empty combobox computed to 24 +
20 + 2 = 46px, while `Input` stays pinned at `h-10`.
This is a sibling of the `ModelSelector` regressions fixed in #28399,
but in the shared combobox primitive, so that fix could not reach these
pages.
## Fix
- Pin the inner cmdk input to `leading-5` and reduce the empty-state
padding to `py-2` (20 + 16 = 36px row).
- Make the wrapper a `flex flex-col justify-center` so the row centers
within `min-h-10`, giving exactly 40px when empty while the selected
state still grows with its badges.
## Validation
- Red-green verified with a temporary geometry probe story: combobox
wrapper 46px before, 40px after (Input 40px in both). The probe was not
committed (FE10) and no new story ships with this PR per review.
- `pnpm test:storybook` for `MultiSelectCombobox`, `IdpSyncPageView`,
`IdpOrgSyncPageView`, and `DynamicParameter`: all pass.
- `biome check` and `tsc -p .` clean.
> 🤖 This PR was written by Coder Agents on behalf of @tracyjohnsonux.
|
||
|
|
6f1d225d2e |
fix(site): improve chat slash menu enter handling, anchoring, and placement (#28411)
When the last token of a follow-up prompt is an absolute filesystem path (e.g. `check /var/log/syslog`), the skill slash menu opened with "No skills match that query." and swallowed Enter, so the prompt could not be submitted without adding a trailing space or quoting the path. Follow-up composers always pass slash commands, so the menu opened for any `/`-token; the Enter handler then consumed the keypress unconditionally while the menu was open. Three fixes (CODAGT-956): - Enter with no selectable match now dismisses the menu and lets the same keypress fall through to the submit handler. While a skill source is still loading (personal skills fetching, or workspace skills unknown), a zero-match Enter stays consumed so a pending fetch cannot let a would-be skill trigger submit as raw text; a no-match Tab still falls through so keyboard users can leave the composer. - The menu is anchored to the composer box instead of a caret-derived rect, so it no longer follows the cursor while the query is typed. - The menu renders directly above the chat input at exactly the composer's width on desktop, matching its pinned-above-composer presentation on small viewports. The caret-rect plumbing (live caret measurement, fixed 1px anchor span, scroll/resize listeners) is removed. Storybook interaction coverage was updated accordingly: `FilteredEmptyEnterClosesAndSubmits` (replaces `FilteredEmptyKeepsMenuOpen`), `TrailingPathEnterSubmits`, `EnterWhileSkillsLoadingDoesNotSubmit`, `MenuStaysAnchoredWhileTyping`, and `OpensAboveAnchorAtAnchorWidth`. Each new story was verified red against the reverted fix and green with it. Remote dogfood UAT validated the Enter/anchoring/placement scenarios and regressions in the real UI across two rounds (round 2 covering the composer-width presentation). Fixes https://linear.app/codercom/issue/CODAGT-956/slash-menu-improvements > 🤖 This pull request was created by Mux on behalf of @ibetitsmike. <!-- mux-attribution: model=claude-opus-4-6 thinking=high --> |
||
|
|
bb7f2f2332 |
fix(site/src): show chat instead of the right panel on small viewports (#28387)
On small viewports, the agent chat page showed the wrong panel. The right panel filled the screen and hid the chat. before: https://github.com/user-attachments/assets/fa3986b8-949a-4aad-8e98-d64316ade3f5 after: https://github.com/user-attachments/assets/bc56359d-2ffe-4f95-b47b-638261379da8 ## Changes - Below the `lg` breakpoint, the page shows one panel at a time. A right panel left open on a wide window used to hide the chat when the window narrowed. The page now shows the chat. - The panel preference is not changed by a resize. When the window widens, the panel comes back. When the user taps a tab while narrow, the panel opens as before. ## Tests - Unit tests for the new `useRightPanelNarrowSuppression` hook cover mount, both breakpoint crossings, and the explicit override. - A new mobile-viewport story asserts the chat is visible when a panel was left open. <details> <summary>Decision log</summary> - The right panel's open state persists in localStorage across all viewport sizes. `max-lg:hidden` on the chat made that stale state hide the chat on narrow windows. - The narrow suppression is transient state, not a write to the persisted preference. A temporary resize keeps the user's desktop layout intact. - The hook adjusts state during render (the React "adjusting state when a prop changes" pattern) instead of an effect, per FE8. - Verified all resize sequences live against a dev deployment: narrow with a panel open, widen to restore, drill in while narrow, and explicit close while narrow. </details> 🤖 This PR was created by [Coder Agents](https://coder.com/docs/ai-coder/agents) on behalf of @bpmct. --------- Co-authored-by: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> |
||
|
|
7970cd6d11 |
fix(site/src/pages/AgentsPage/components/ChatElements/tools): stop completed process_output rows animating forever (#28414)
Since #28300, `process_output` tool rows in the Agents chat transcript fed the result payload's `running: true` flag into the row's active state. That flag is a frozen snapshot of whether the polled process was still alive, so for long-lived background processes (a dev server that never exits) every completed poll kept its shimmer label and spinner animating forever, even after the agent went idle. `ToolCall.Root` now receives the actual tool-call status, so the shimmer and spinner stop when the poll completes. The `running: true` snapshot still controls what it was meant to: label tense ("Checking npm start" while alive vs "Checked npm start" after exit) and kill-signal badge visibility. The existing `ProcessOutputStillRunningResult` story now also asserts the running spinner is absent on a completed poll (verified red-green: reintroducing the bug fails exactly that story), and the intent story asserts the spinner is present while the tool call is genuinely running. > [!NOTE] > Mux (AI agent) authored this PR on behalf of @ibetitsmike. <!-- mux-attribution: model=claude-opus-4-6 thinking=high --> |
||
|
|
c940284e18 | fix(site): freeze motion animations during Pixel captures (#28396) | ||
|
|
b14bce12bf |
fix(site/src/pages/AgentsPage/components): fix model selector trigger layout on form-style pages (#28399)
Selecting a model on form-style pages (deployment AI settings such as `/ai/settings/coder-agents`, the personal agent settings override rows, and the advisor override) rendered a broken trigger: the provider icon pinned to the far-left edge, the model name floating in the center, and the chevron on the right. At desktop widths the field also collapsed to content height (~22px) next to the 40px Clear/Save buttons. Two defects in the shared `ModelSelector` trigger: 1. #28330 added the provider icon as a third direct flex child of the trigger button. Form call sites override the trigger with `justify-between` plus a width wider than its content, which spread icon, label, and chevron apart. The icon and label now form a single flex child so they stay adjacent under any `justify-*` override; the chevron stays a direct child because the `[&>svg]` trigger styles target it, and the label still truncates via `min-w-0`. 2. The base classes carried composer-pill responsive variants (`md:h-auto`, `md:w-auto`, `md:shrink-0`). tailwind-merge resolves conflicts per modifier, so `md:h-auto` silently defeated the call sites' `h-10` at the `md` breakpoint (and `md:w-auto` defeated `w-[22rem]` on the advisor row). The pill's responsive sizing now lives at its call site, so plain `h-10`/width overrides win everywhere. The composer pill is visually unchanged. Also fixes the provider icon shrinking next to long model names: the icon's `span` wrapper was a plain flex item whose automatic minimum size collapses (preflight's `img { max-width: 100% }` scales the icon down as the wrapper shrinks). The wrapper is now a `shrink-0` flex container, so the label truncates while the icon keeps its 12px size (verified red-green with a temporary probe: 7.3px without the fix). Adds `FormFieldTriggerKeepsIconAndLabelAdjacent` and `FormFieldTriggerTruncatesLongModelName` stories reproducing the form-style trigger for visual snapshot coverage. Both defects were verified red-green during development with temporary geometry probes (icon-label gap 63.5px and height 30px without the fixes; gap 4px and height 40px with them); per FE10 the committed story carries no geometry assertions and the pixel snapshot guards the layout. Also recolors the advisor "Uses / turn" and "Max tokens" field labels from `content-placeholder` to `content-secondary` on the same settings surface. Remote dogfood UAT ran against this branch across desktop and narrow viewports and passed. > 🤖 Mux acted on behalf of @ibetitsmike to create this PR. |
||
|
|
93d5f82849 |
fix(site/src/pages/AgentsPage/components/ChatsSidebar): hide kebab while archiving spinner shows (#28393)
Since #28326 made the row actions trigger (kebab) persistently visible on the selected chat, it overlapped the archiving spinner: both render right-aligned in the same 28px box (the kebab is `absolute inset-0` over the timestamp/spinner slot), so archiving the selected chat produced a garbled spinner-plus-dots glyph. The overlap could previously only occur transiently on hover; the persistent kebab made it guaranteed on the active row. Render the kebab only when the row is not showing the archiving spinner, restoring the intent of the existing `ArchivingShowsSpinnerOnly` story (which only covers a non-selected row, so it never caught this). <details> <summary>Diagnosis and validation</summary> **Diagnosis** - The only right-side loader in a sidebar row is the `Spinner` rendered by `ChatTreeNode` when `isArchiving && archivingChatId === chat.id`. - #28326 added `isActiveChat && "opacity-100"` to the kebab trigger, which shares the same `w-7` box as the spinner. The spinner branch never suppressed the trigger, only the timestamp span had the hover/active `hidden` classes. - Result: on the selected chat, `EllipsisVerticalIcon` and the 8-leaf `Spinner` render on top of each other for the entire archive mutation. **Fix** - `ChatTreeNode.tsx`: gate the `DropdownMenu` on `!isArchivingThisChat` so the spinner and trigger are mutually exclusive. Row context-menu actions remain available and are already disabled while archiving. **Validation** - `pnpm test:storybook src/pages/AgentsPage/components/ChatsSidebar/ChatsSidebar.stories.tsx`: 53 passed - `make pre-commit`: pass (lint/ts, lint/go, gen, fmt, build) </details> _Generated by Coder Agents on behalf of @tracyjohnsonux._ |
||
|
|
b605446016 |
feat(site/src/pages/AgentsPage): dim unselected sidebar chat titles (#28382)
## Summary Subtle visual tweak to the agents sidebar chat list. Chat titles remain `text-content-primary`, but titles for chats that are not currently selected now render at 85% opacity. The selected chat keeps full opacity (and its existing `font-medium` weight), making the active chat stand out more clearly. ## Changes - `ChatTreeNode.tsx`: apply `opacity-85` to the title span when the chat is not active, keeping `font-medium` for the active chat. `opacity-85` is a valid Tailwind 3.4 utility and is already used elsewhere in the codebase. --- _This PR was generated by Coder Agents on behalf of @tracyjohnsonux._ |
||
|
|
e58a603949 |
fix(site): prevent duplicate menu when right-clicking chat kebab (#28391)
Right-clicking the kebab (⋮) button on a chat in the agents sidebar was showing two menus: the kebab's own dropdown **and** the row-level right-click context menu. The kebab lives inside the row's \`ContextMenuTrigger\`, so the \`contextmenu\` event on the button bubbles up and opens the row's context menu on top of the dropdown that the button already owns. Fix: swallow \`contextmenu\` on the kebab \`Button\` so only its dropdown is associated with the button. Right-clicking anywhere else on the row still opens the row context menu as before. > Generated with Coder Agents on behalf of @tracyjohnsonux. --------- Co-authored-by: Coder Agents <noreply@coder.com> |
||
|
|
05b78a0902 |
fix(site): scroll per-script logs selector on overflow (#28353)
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell. Resolves [DEVEX-721](https://linear.app/codercom/issue/DEVEX-721/the-logs-selector-per-startup-script-needs-to-scroll-on-overflow). Cap the overflow menu height and let it scroll, matching the existing pattern already used by `DownloadSelectedAgentLogsButton` (`max-h-56 overflow-y-auto`). No component swap needed, the `DropdownMenu` was the right primitive, it just lacked height/scroll constraints. ```diff -<DropdownMenuContent align="end"> +<DropdownMenuContent align="end" className="max-h-56 overflow-y-auto"> ``` <img width="504" height="325" alt="image" src="https://github.com/user-attachments/assets/363d9bc0-e3c3-4355-8d05-abb8c650a37a" /> <details> <summary>Investigation / decision log</summary> - The "logs selector per startup script" is the overflow kebab menu in `site/src/modules/resources/AgentRow.tsx`. When per-script log tabs don't fit horizontally, `useKebabMenu` moves the extras into a Radix `DropdownMenu` rendered as `DropdownMenuRadioItem`s. - `menuContentClass` (`site/src/components/DropdownMenu/menuClasses.ts`) is `z-50 min-w-48 overflow-hidden ...` with no max-height, so an overlong menu is clipped instead of scrolling. - `DownloadSelectedAgentLogsButton.tsx` already solves the identical problem on its own menu with `className="max-h-56 overflow-y-auto"`, so we reused that for consistency. - Considered switching to the `Select` component (which has `SelectScrollUpButton`/`SelectScrollDownButton` chevrons) but that's a larger re-model of the trigger + items and reconciliation with the `Tabs`/`useKebabMenu` wiring. Deferred in favor of the minimal fix. </details> |
||
|
|
376217423d |
fix(site/src/pages): use RotateCcwIcon for update actions (#28183)
Closes [DES-22067](https://linear.app/codercom/issue/DES-22067/update-and-restart-action-uses-a-cloud-icon). The "Update and restart" / "Update and start" actions used `CloudIcon`, which reads as a cloud/network concept instead of an update-and-restart build action. Switch them to `RotateCcwIcon`, which the UI already uses for: - the `Restart` button on the workspace page - the `Update` action in `WorkspaceOutdatedTooltip`, `AgentOutdatedTooltip`, and `SubAgentOutdatedTooltip` Call sites changed: - `WorkspacePage/WorkspaceActions/Buttons.tsx` - `UpdateButton` - `WorkspacesPage/WorkspacesTable.tsx` - "Update and start workspace" and "Update and restart workspace" primary actions - `WorkspacesPage/WorkspacesPageView.tsx` - batch "Update" menu item The `requireActiveVersion` variant still uses `PlayIcon`, since that path starts the workspace on the active version rather than offering a choice. Worth a separate design pass if that should change too. <details> <summary>Investigation notes</summary> Verified against `origin/main` (`af90d8e2be`): no prior PR referenced DES-22067 and all three `CloudIcon` usages were still present. `grep` confirms no `CloudIcon` usages remain in `site/src` after this change. Icon conventions found in the codebase: - `RotateCcwIcon`: restart, update (outdated tooltips), retry (RetryButton, TasksTable, TaskPage, HealthLayout) - `RefreshCwIcon` / `RefreshCcwIcon`: softer "reload data" flows (terminal, secrets, prebuilds invalidate, inbox, git panel) `data-testid="bulk-action-update"` is preserved on the batch update item, so e2e coverage is unaffected. </details> --- Generated by Coder Agents on behalf of @designertyler. |
||
|
|
47ffa87fa5 |
fix: <BuildingTemplateLoader /> icon sizes (#27733)
|
||
|
|
8eb1e43f0b |
feat: apply reasoning effort and surface thinking blocks for Google chat models (#28273)
## Problem
Selecting a reasoning effort on a Google (Gemini) chat model either got
dropped or broke the request outright, and even when the request
succeeded, Gemini chats never showed thinking blocks:
- The native `fantasygoogle` path had no case in `applyReasoningEffort`
at all, so effort was silently ignored. With only `include_thoughts`
configured, Gemini 3+ models default to no thinking, so chats produced
zero reasoning tokens.
- The path chat generation actually uses today routes Google providers
through aibridge as openai-compat clients and forwarded the raw effort
string as `reasoning_effort`. Google's OpenAI-compatible endpoint
validates that value against the model's supported thinking levels
instead of clamping, so out-of-range efforts fail the whole request with
HTTP 400. Verified against the live Gemini API: `gemini-3.1-pro-preview`
accepts low/medium/high and rejects none/minimal/xhigh/max. Because the
config's default effort is sent whenever the user selects nothing, a
config whose default falls outside the model's supported set failed
every no-selection message.
- Google's OpenAI-compatible endpoint never emits thought text for
`reasoning_effort` requests. Thoughts require
`extra_body.google.thinking_config` with `include_thoughts`, which is
mutually exclusive with `reasoning_effort`, and even then the thought
deltas come back inline in `content` (marked
`extra_content.google.thought`, wrapped in `<thought>` markers) rather
than in the `reasoning_content` field OpenAI-compatible clients parse.
So no reasoning parts were ever produced, persisted, or rendered for
Gemini chats (verified live in UAT).
## Fix
Reasoning effort:
- Map the global reasoning effort scale onto Google's `thinking_level`
in `applyReasoningEffort`, gated to Gemini 3+ models and clamped to each
model's supported subset (3.0 Pro launched with LOW/HIGH only, 3.1 Pro
added MEDIUM, Flash accepts all four through 3.6 while 3.7 and the
latest flash aliases drop MINIMAL, image models accept HIGH plus MINIMAL
for flash). Pre-Gemini-3 models reject `thinking_level`, so effort
degrades to a no-op for them and a config-pinned level is dropped at
call time instead of failing every generation.
- Add `thinking_level` to `ChatModelGoogleThinkingConfig` so admins can
pin a level for configs without user-selectable effort. A resolved
per-turn effort overrides the pinned level; an explicit
`thinking_budget` wins over both since the Google API rejects requests
that set `thinking_budget` and `thinking_level` together, and config
validation now rejects that combination up front. The admin panel picks
up the new field automatically through the generated model-options
schema.
Thinking blocks on the openai-compat path (the path chat generation
uses), translated in both directions at the existing Gemini transport
seam:
- chatd's OpenAI-compat request patch swaps `reasoning_effort` for an
equivalent `extra_body.google.thinking_config` with `include_thoughts`
enabled: `thinking_level` on Gemini 3+ (clamped to the model's supported
subset), `thinking_budget` on earlier models (effort `none` maps to
budget 0, which disables thinking). Non-Gemini models and non-Google
routes are untouched.
- The same transport rewrites Google's responses so thought output
surfaces as `reasoning_content`, which fantasy already converts into
reasoning parts: streaming bodies are rewritten per SSE line (preserving
streaming latency), JSON bodies in place, with the
`<thought>`/`</thought>` markers stripped at the transitions.
- aibridge's chat-completions interception preserves the `extra_body`
passthrough that openai-go's typed params silently drop, forwarding it
to Google upstreams only, so the chatd -> aibridge -> Google route
carries the thinking config end to end.
## Validation
- `go test ./coderd/x/chatd/... ./aibridge/... ./internal/googleopenai
./codersdk` pass; golangci-lint clean on touched packages.
- New unit tests cover the effort-to-level mapping for every effort
value, per-model clamping on both paths, the effort-to-thinking_config
request swap (level, budget, pinned-config precedence, non-Gemini
passthrough), SSE and JSON thought-response rewriting (fixtures mirror
live captures, including tool-call and no-thought streams), an
end-to-end fake-SSE test through the fantasy pipeline proving reasoning
deltas emerge with markers stripped, and aibridge `extra_body`
preservation (including proof the typed round trip drops it).
- Verified live against `generativelanguage.googleapis.com` on
`gemini-3-flash-preview` and `gemini-2.5-flash` through the real
chatprovider stack: reasoning deltas stream through with markers
stripped and answer text intact.
- Live UAT on a dogfood deployment (remote Coder Agents chat, 4 rounds):
effort clamping verified end to end (rounds 1-2), the missing-thoughts
limitation isolated (round 3), and the fix validated (round 4):
expandable, readable thinking blocks render for
`gemini-3.1-pro-preview`, `gemini-3-flash-preview`, and
`gemini-2.5-flash` at effort=high; no `<thought>` marker leaks;
effort=none produces no block and no 400; multi-turn history replay
works with a thinking block on each turn.
- `make gen` regenerated `typesGenerated.ts` and
`chatModelOptionsGenerated.json`; `pnpm -C site lint:types` passes.
> 🤖 Mux acted on Mike's behalf to create this pull request.
<!-- mux-attribution: model=claude-opus-4-6 thinking=high -->
|
||
|
|
ab6601086a |
fix: gate group settings page and fix unauthorized group patch (#28314)
## Problem A user without `group:update` (e.g. the `Coder Agents User` role) could open the group settings page by direct URL and edit the budget. Saving then failed with a misleading error: > Failed to add or remove non-existent group member — execute transaction: update group by ID: unauthorized: rbac: forbidden. The authorization failure was misclassified as a missing group member, and the settings page was not gated on the required permission. ## Changes - Return the standard vague 404 for authorization failures in the group PATCH handler, so the response is accurate and does not leak resource existence. - Use a generic message for the remaining 400 branch, since it covers various transaction failures rather than only member add/remove. - Gate the group settings page on `group:update`. The settings tab is already hidden without the permission, but the page was reachable by direct URL and rendered an editable form that always failed on save. It now shows the standard permission dialog. Fixes [AIGOV-603](https://linear.app/codercom/issue/AIGOV-603/hide-group-budget-controls-without-permission). > [!NOTE] > Generated by Coder Agents, reviewed by @ssncferreira. |
||
|
|
015d6a6e06 | feat(site): add provider icon to chat model selector (#28330) | ||
|
|
d58788cb23 |
fix: clarify AI model price configuration (#28372)
## Description Model prices displayed in the Agents model form are read-only because they are managed by AI Gateway. Add guidance and a link to the model price configuration documentation, and rename the related documentation subsection to **Configure model prices**. > [!NOTE] > Generated with Coder Agents by @ssncferreira. |
||
|
|
50ecd6959f |
feat: add a source filter to the AI model price list (#28329)
## Description With prices from the price book and prices set through the API both in the table, `list` showed only the price in effect and gave no way to tell where it came from. There was also no way to see the price book's entry for a model that had been overridden, since the listing resolves to the custom price. This adds a `source` column to the output and a `--source` filter for narrowing to one source, or to every row with `all`. Note: `--source` selects rows rather than models. A model carrying both a price book price and a custom one appears under either value, so the two filtered listings do not sum to the unfiltered one. `--source all` reports both rows at once. ## Changes - Add `Source` to `codersdk.AIModelPrice` and `AIModelPricesFilter`, and map it in `db2sdk`, which previously dropped it. - Filter `GetAIModelPrices` by source before resolution, so the price book's row for an overridden model stays reachable. - Accept `all` as a source, which reports every price a model holds instead of resolving to one. - Accept a `source` query parameter on the list endpoint, rejected with a 400 when outside the enum. - Add `--source` to the list command and a `source` column to its output. - Document the column and the filter, and lead the price section with `coder exp ai-model-prices list`. - Distinguish a deployment's default prices from the price book that ships with each release. Closes https://linear.app/codercom/issue/AIGOV-593 > [!NOTE] > Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira |
||
|
|
4a7a0a3348 | feat: label process_output rows with the process command (#28300) | ||
|
|
eed420a0eb | fix(site/src/pages/AgentsPage): clear streamed message when a chat errors (#28277) | ||
|
|
5e0710de92 |
chore: refresh AI model price book (#28365)
## Price book changes 61 models added, 14 models removed, 51 models changed. <details> <summary>Added</summary> - azure/claude-mythos-5 - azure/claude-opus-4-7 - azure/kimi-k2.7-code - bedrock/global.openai.gpt-5.6-luna - bedrock/global.openai.gpt-5.6-sol - bedrock/global.openai.gpt-5.6-terra - bedrock/xai.grok-4.6 - copilot/gemini-3.7-flash - copilot/grok-4.6 - copilot/kimi-k3 - copilot/mai-code-1.1-flash - google/gemini-3.7-flash - openrouter/bytedance-seed/seed-2-1-turbo - openrouter/bytedance-seed/seed-2.0-code - openrouter/deepseek/deepseek-v4-pro-0813 - openrouter/dots-studio/dots-3-note-preview:free - openrouter/google/gemini-3.7-flash - openrouter/inclusionai/ling-3.0-flash - openrouter/liquid/lfm-2.5-2.6b:free - openrouter/meta/muse-glimmer-30b - openrouter/meta/muse-spark-1.2 - openrouter/nvidia/nemotron-3.5-lightning - openrouter/nvidia/nemotron-3.5-lightning:free - openrouter/qwen/qwen3.8-2.4t-a95b - openrouter/qwen/qwen3.8-27b - openrouter/sakana/sakana-namazu - openrouter/upstage/solar-pro4 - openrouter/x-ai/grok-4.6 - openrouter/z-ai/glm-5.2:free - openrouter/z-ai/glm-5.3 - openrouter/~z-ai/glm-latest - vercel/alibaba/qwen3.8-2.4t-a95b - vercel/alibaba/qwen3.8-27b - vercel/deepseek/deepseek-v4-pro-0813 - vercel/google/gemini-3.7-flash - vercel/inclusionai/ling-3.0-flash - vercel/meta/muse-glimmer-30b - vercel/meta/muse-spark-1.2 - vercel/meta/muse-spark-1.2-contributor - vercel/nvidia/nemotron-3.5-lightning - vercel/openai/gpt-4.1-fast - vercel/openai/gpt-4.1-mini-fast - vercel/openai/gpt-4.1-nano-fast - vercel/openai/gpt-4o-fast - vercel/openai/gpt-4o-mini-fast - vercel/openai/gpt-5-fast - vercel/openai/gpt-5-mini-fast - vercel/openai/gpt-5.1-thinking-fast - vercel/openai/gpt-5.2-fast - vercel/openai/gpt-5.3-codex-fast - vercel/openai/gpt-5.4-fast - vercel/openai/gpt-5.4-mini-fast - vercel/openai/gpt-5.5-fast - vercel/openai/gpt-5.6-luna-fast - vercel/openai/gpt-5.6-sol-fast - vercel/openai/gpt-5.6-terra-fast - vercel/openai/o3-fast - vercel/openai/o4-mini-fast - vercel/sakana/namazu - vercel/xai/grok-4.6 - vercel/zai/glm-5.3 </details> <details> <summary>Removed</summary> - anthropic/claude-opus-4-1 - anthropic/claude-opus-4-1-20250805 - google/gemini-2.0-flash - google/gemini-2.0-flash-lite - google/gemini-3-pro-preview - openrouter/ai21/jamba-large-1.7 - openrouter/inclusionai/ling-3.0-flash:free - openrouter/mancer/weaver - openrouter/openai/gpt-5.3-chat - vercel/anthropic/claude-opus-4.1 - vercel/inclusionai/ling-3.0-flash-free - vercel/openai/gpt-5.1-instant - vercel/openai/gpt-5.3-chat - vercel/zai/glm-4.6v </details> <details> <summary>Changed</summary> - azure/gpt-5.6-luna - azure/gpt-5.6-sol - azure/gpt-5.6-terra - bedrock/openai.gpt-5.6-sol - openrouter/deepseek/deepseek-chat-v3-0324 - openrouter/deepseek/deepseek-v3.1-terminus - openrouter/deepseek/deepseek-v4-flash - openrouter/deepseek/deepseek-v4-flash-0731 - openrouter/deepseek/deepseek-v4-pro - openrouter/google/gemini-3.6-flash - openrouter/google/gemma-4-31b-it - openrouter/gryphe/mythomax-l2-13b - openrouter/minimax/minimax-m2.5 - openrouter/minimax/minimax-m2.7 - openrouter/mistralai/mistral-small-3.2-24b-instruct - openrouter/moonshotai/kimi-k2.5 - openrouter/moonshotai/kimi-k2.6 - openrouter/moonshotai/kimi-k2.7-code - openrouter/openai/gpt-5.6-luna - openrouter/openai/gpt-5.6-luna-pro - openrouter/openai/gpt-5.6-sol - openrouter/openai/gpt-5.6-sol-pro - openrouter/openai/gpt-5.6-terra - openrouter/openai/gpt-5.6-terra-pro - openrouter/openai/gpt-oss-120b - openrouter/qwen/qwen-plus-2025-07-28:thinking - openrouter/qwen/qwen2.5-vl-72b-instruct - openrouter/qwen/qwen3-14b - openrouter/qwen/qwen3-235b-a22b-2507 - openrouter/qwen/qwen3-30b-a3b - openrouter/qwen/qwen3-coder-30b-a3b-instruct - openrouter/qwen/qwen3-vl-235b-a22b-thinking - openrouter/qwen/qwen3-vl-30b-a3b-instruct - openrouter/qwen/qwen3.5-35b-a3b - openrouter/qwen/qwen3.6-27b - openrouter/qwen/qwen3.6-35b-a3b - openrouter/tencent/hy3-preview - openrouter/thinkingmachines/inkling - openrouter/thinkingmachines/inkling-small - openrouter/z-ai/glm-5 - openrouter/z-ai/glm-5.2 - openrouter/~deepseek/deepseek-v4-flash-latest - openrouter/~google/gemini-flash-latest - openrouter/~moonshotai/kimi-latest - openrouter/~openai/gpt-latest - openrouter/~x-ai/grok-latest - vercel/deepseek/deepseek-v4-flash - vercel/deepseek/deepseek-v4-pro - vercel/google/gemini-3.6-flash - vercel/openai/gpt-5.6-sol - vercel/zai/glm-5.2 </details> ## Review notes Regenerated by `make gen/aibridge-prices` from the live [models.dev](https://models.dev) catalog. Both artifacts come from one snapshot, so they ship together: - `coderd/aibridge/prices/data/prices.json` - `site/src/pages/AgentsPage/components/ChatModelAdminPanel/knownModels/knownModelsGenerated.json` These are customer-visible cost numbers taken from upstream data, so this PR is never merged automatically. The summary above lists what moved; check the diff for exact figures before approving. Opened automatically by the [aigateway-prices-refresh workflow](https://github.com/coder/coder/actions/runs/32352560765). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
e5deb57079 |
feat(site): show total agent hours consumption on the licenses page (#27953)
<img width="1195" height="165" alt="Screenshot 2026-08-19 at 5 31 17 PM" src="https://github.com/user-attachments/assets/d1e49e5f-1af6-4221-87fe-ae40997f0ae7" /> Adds a "Total agent hours" consumption panel to the Licenses page, surfacing the `agent_runtime_hours` entitlement introduced by the #27983 / #27984 / #27985 stack. The new `TotalAgentHoursCard` renders full width directly above the Agent Workspace Builds panel. It shows a usage bar with Used/Limit labels and an info tooltip next to the title. The bar color and tooltip copy track the same thresholds the backend uses for license warnings: green below the soft limit, orange once usage reaches the soft limit, and red once usage reaches the purchased allocation (values at or beyond the threshold count as reached, matching `appendAgentRuntimeHoursWarning`). When a soft limit exists, the tooltip states the actual soft-limit percentage computed from `soft_limit / limit`. The panel is hidden entirely when the feature is disabled, which is how the backend reports a zero-hour allocation. The usage period dates from `Feature.usage_period` render above the bar (start left, end right), matching the Agent Workspace Builds panel, so admins can tell which license period the consumption covers. The backend always attaches a period to an enabled feature; a missing or unparsable one just omits the dates row. The CODAGT-855 link to the per-user usage view is deferred: that view (F3, CODAGT-861) and the API it needs (F2, CODAGT-857) are not built yet, so there is no route to target. The link lands with F3. An enabled feature with `limit` omitted is the unlimited allocation (license claim `-1`, coder/license#49): the card shows a full neutral bar with "Limit: Unlimited" and the tooltip without threshold copy, mirroring `SeatUsageBarCard`'s unlimited state. The invalid-limits error is kept for negative limits, which can only come from a decoding bug. Storybook stories cover the green/orange/red states, the capped over-allocation bar, missing usage data, the missing usage period, the hidden state, the unlimited state, the invalid-limit error state, and each tooltip message via play interactions. The page view story verifies the panel renders above Agent Workspace Builds. Usage renders with exactly one decimal (e.g. `435.8`, `1,000.0`), derived from #27985's `actual_ms` field and floored to tenths with integer math. The same floored value drives the reached states, so the displayed number and the bar color flip at the same instant; a fraction past the allocation now trips the red state (`1,000.1 >= 1,000`), pinned by the `ReachedAllocationByFraction` story. The limit and hard cap labels stay whole because the license claims are whole hours. Stacked on #27985. closes [CODAGT-855](https://linear.app/codercom/issue/CODAGT-855/f1-runtime-hours-consumption-on-the-licenses-page). |
||
|
|
64f26ef387 |
chore: add supabase icon (#28327)
Adds the Supabase icon to the built-in icons for use with external auth and modules. - Adds `site/static/icon/supabase.svg` (official Supabase logo) - Registers icon in `site/src/theme/icons.json` The icon is full-color with good contrast on both light and dark backgrounds, so no special handling in `externalImages.ts` is needed. |
||
|
|
5be2fa7974 |
feat(site/src/pages/AgentsPage/components/ChatsSidebar): move PR icon to diff line, center unread dot, persist kebab (#28326)
Three chat-sidebar row updates cherry-picked from #28317 (that PR stays open as the full riff), plus one removal: - **PR icon moves to the diff line.** The linked pull request icon (open / draft / merged / closed) no longer replaces the status icon when the chat is idle. It renders on the second line next to the `+additions -deletions` stats, in both the sidebar tree and the chat search results, regardless of chat status. - **Unread dot alignment.** The unread dot is centered in a box matching the kebab's width, so it sits on the same axis as the actions trigger instead of shifting when the hover swap happens. - **Persistent kebab on the selected chat.** The row actions trigger stays visible on the active chat (no hover required, reachable on touch devices); the timestamp is hidden there. Other rows keep the hover swap. - **Subagent count badge removed.** The `N` + bot icon added to the metadata line in #28234 no longer renders, making room for the PR icon and diff stats. The "Show subagents" menu toggle and expand behavior are unchanged. Status and PR icons carry `role="img"` with accessible labels ("Idle", "Pull request merged", ...). <details> <summary>Implementation notes</summary> - `statusConfig.ts`: `getChatDisplayConfig(chat)` now returns the status icon plus a separate `prIcon` config (icon, className, accessible label) instead of letting the PR icon take precedence over the status. Labels added for all status and PR states. - `ChatTreeNode.tsx` / `ChatSearchResults.tsx`: render `prIcon` on the second line before the line stats. - Kebab persistence reuses the existing `isActiveChat` value: `opacity-100` on the trigger, `hidden` on the timestamp for the active row. - `ChatTreeNode.tsx`: the on-row subagent count badge (`childIDs.length` + `BotIcon`) is removed; the `SubagentsMenuToggle` story only asserts the menu toggle, so it still passes. - Not included from #28317: the subagent status icon variants (`CheckCheckIcon` / `SubagentsLoaderIcon`, the `hasSubagents` param, and the `WithSubagentStatusIcons` story). Validation: - `pnpm run lint` (biome, tsc, knip, circular deps, compiler): pass - Story tests: 96 passed (4 files) including the updated `WithPRStateIcons` label assertions and the new `ActiveChatKebabPersistent` story - Unit suite for the sidebar area: 65 passed - `make pre-commit` (via git hook): pass </details> _Generated by Coder Agents on behalf of @tracyjohnsonux._ |
||
|
|
24b83dc727 |
feat: add organization picker to MCP server settings pages (#28150)
Adds an organization picker to the MCP server settings pages so
multi-org deployments can manage every organization's MCP servers
through the UI, not just the default organization's. Closes CODAGT-714
(scoped to MCP servers only; model configs stay deployment-wide until
CODAGT-709 lands).
## Stack Context
Top of the CODAGT-711/717/712/806 stack: MCP server configs are
org-scoped with RBAC, audit logging, per-config ACLs, and owner-scoped
user tokens. This layer makes the admin UI org-aware, resolving the
"default org only" limitation documented in the bottom PR.
## Summary
- The MCP servers list and add pages resolve the working organization
from an `org` search param (falling back to the default org), and render
an organization picker built on the existing `OrganizationAutocomplete`.
The picker only appears when more than one organization is visible, so
single-org deployments see no change.
- Selecting an organization rewrites the search param, refetches the
list for that org, and threads through to the add page so creation posts
to the selected organization.
- The update page now loads the server by ID (new organization-scoped
`GET /api/experimental/organizations/{organization}/mcp-servers/{id}`
frontend binding) instead of listing the default org and searching
client-side, so servers from any organization can be edited. Post-save
navigation returns to the owning organization's list via the server's
`organization_id`.
- Removes the now-unused `getDefaultOrganizationId` helper.
- Review hardening: organization display labels are disambiguated with
the unique org name when effective display names collide (picker
options, trigger text, and accessible name). Access is no longer
restricted to deployment admins: the pages, sidebar item, and nav entry
accept org-scoped `mcp_server_config` permissions, the picker lists only
organizations the user can view, content and affordances (add, edit,
delete) are gated on the selected organization's permissions, and
deployment admins keep full access everywhere.
## Testing
Storybook interaction stories cover: picker hidden with one org,
switching orgs refetches and swaps list contents, add posting to the
URL-selected org, add posting after an in-form org switch (asserting
typed form state survives the switch), and update loading by ID.
Red-green verified by breaking the search-param resolution and observing
the new stories fail.
> Mux acted on Mike's behalf for this PR.
|
||
|
|
8203b2ebaf | feat!: hide Coder Tasks behind the enable-ai-tasks flag (#28008) | ||
|
|
7ca7c30f40 |
feat: add group and user ACLs to MCP server configs (#27944)
Adds group and user ACLs to org-scoped MCP server configs so
organizations can restrict specific MCP servers to subsets of members,
mirroring the template ACL pattern.
## Summary
- Migration adds `group_acl`/`user_acl` JSONB columns (nested `{"<id>":
{"permissions": [...]}}` shape) and seeds every existing config with its
organization's Everyone group read entry (the Everyone group ID equals
the org ID), so member access is unchanged by default. Creation seeds
the same entry.
- The blanket org-member read grant from the base PR is replaced by ACL
evaluation: Rego requires org membership for every ACL grant, and
`GetAuthorizedMCPServerConfigs` compiles ACL-aware SQL filters.
- New `ActionShare` (org admins) gates `GET|PATCH
/api/experimental/organizations/{organization}/mcp-servers/{mcpserverconfig}/acl`
(nested under the organization like the rest of the config surface).
PATCH validates principals against the config's organization, merges
sparse updates under a row lock, stamps `updated_by`/`updated_at`, and
is audited as a Write with `Old` captured before authorization. A config
deleted concurrently between the middleware fetch and the locked
re-fetch is concealed as 404, matching the update and delete handlers.
ACL columns are tracked in the audit table.
- ACL management is available in all editions (no enterprise
entitlement), documented in the MCP servers page. Revoking an ACL does
not retro-strip already-selected configs from existing chats; new
selection is blocked at chat create.
- Force On respects the ACL: the forced set is loaded as the chat owner,
so a `force_on` server whose ACL denies the owner never attaches at
create, send, or generation time.
- No rolling-upgrade machinery: upgrades run in scheduled maintenance
downtime, so the migration only backfills existing rows; the API sets
the Everyone read grant explicitly on every insert.
Stacked on #27943. Part of the MCP org-separation stack.
Closes https://linear.app/codercom/issue/CODAGT-712
UAT: verified on a dogfood instance with two members and a custom group:
Everyone-seed default visibility, group grant with Everyone removal
(non-member loses list/fetch/selection), foreign-principal rejection,
non-admin share denial (audited 403 / concealed 404), user_acl restore,
and audited ACL diffs.
> Mux (AI agent) authored this PR on Mike's behalf.
<!-- mux-attribution: model=claude-fable-5 thinking=high -->
|
||
|
|
299e72ad30 |
feat: audit MCP server config changes (#27943)
Adds enterprise audit logging for MCP server config create, update, and
delete, with strict secret redaction. MCP configs hold credentials
(OAuth2 client secrets, API keys, custom headers), so admin changes to
them need an audit trail.
## Summary
- `enterprise/audit/table.go` gains an `MCPServerConfig` entry
enumerating every column: `oauth2_client_secret`, `api_key_value`, and
`custom_headers` are `ActionSecret` (never appear in diffs); dbcrypt
`*_key_id` bookkeeping, IDs, and timestamps are ignored; the remaining
config fields, including the endpoint URL fields, are tracked so
auditors can see which endpoints a config points at.
- Type registration in `coderd/audit` (diff, request, resource target
with org attribution), `codersdk/audit.go`, and a `resource_type` enum
migration.
- Handlers wire `audit.InitRequest`: create records `New`; update and
delete record `Old` from the param middleware before the
write-authorization check, so a readable-but-not-writable caller
produces an audited 403 while read-denied callers stay concealed as
unaudited 404s.
- Tests: create/update/delete audit entries, write-denied and
delete-denied 403 auditing, cross-org concealment producing zero
entries, and a serializer-level regression test proving none of the
three secret classes can reach a serialized diff.
- Review round: MCP config audit entries link to
`/ai/settings/mcp-servers/{id}`, audit table comments are trimmed per
review, and a fault-injection test pins that a config row surviving a
failed post-discovery credential update still gets its creation audit
entry.
Stacked on #27942 (org-scoped MCP configs). Part of the MCP
org-separation stack.
Closes https://linear.app/codercom/issue/CODAGT-717
UAT: verified on a trial-licensed dogfood instance: audit entries for
the full CRUD lifecycle with correct actor/org/target, redacted secrets
in the update and OAuth2 create diffs, and a full plaintext scan of the
audit dump finding zero secret leaks.
> Mux (AI agent) authored this PR on Mike's behalf.
<!-- mux-attribution: model=claude-fable-5 thinking=high -->
|
||
|
|
29c129e2d0 | fix(site): fix price threshold display and debounce model price lookup (#28298) |