Commit Graph
5919 Commits
Author SHA1 Message Date
Michael Suchacz 3c3240836e refactor: consolidate viewport hooks and remove defineProperty matchMedia stub (#28460)
Follow-up to review feedback on #28387.

That PR added three hooks for one feature (`useIsBelowLgViewport`,
`useIsBelowMdViewport`, and the local single-use
`useRightPanelNarrowSuppression`) and stubbed `window.matchMedia` in
tests with `Object.defineProperty`.

- Replace the two single-purpose viewport hooks with one generic
`useMediaQuery(query)`. Callers pass the shared Tailwind-aligned query
constants from `utils/mobile.ts`. The one new hook replaces the two
deleted ones (net -1).
- Inline `useRightPanelNarrowSuppression` into `AgentChatPage`, its only
consumer, and drop its `renderHook` unit suite. The behavior stays
covered by the narrow-viewport stories; the widening-restore case moved
into the `NarrowingSuppressesExpandedPanel` play function.
- Rework `testHelpers/matchMedia.ts` to install the stub with `spyOn`
from `storybook/test` instead of `Object.defineProperty`. The helper is
story-only now (stories run in real Chromium; jsdom has no
`matchMedia`), so any future unit test needing a stub should use
`vi.stubGlobal` directly.
- Encode the feedback in the canonical FE contract so it gets caught
during development: `.claude/docs/FRONTEND_PATTERNS.md` now bans new
React hooks when an existing hook, a plain function, or component state
suffices (FE3) and bans replacing browser globals with
`Object.defineProperty` in tests or stories (FE9: `vi.stubGlobal` /
`spyOn`), and notes that `renderHook` suites for stateful UI hooks
belong in the consuming component's story (FE1). The `frontend-review`
skill checklist flags all three. `site/AGENTS.md` is unchanged since it
already defers to the patterns doc.

Validation: `pnpm check`, `pnpm format:check`, `pnpm lint` (biome,
types, knip, circular deps, compiler check), `AgentChatPage.test.ts` (70
passed), story runs for `AgentChatPage.stories.tsx` and
`WorkspacePill.stories.tsx` (49 passed in Chromium).

> Xum acted on Mike's behalf (@ibetitsmike).

<!-- xum-attribution: model=claude-opus-4-6 thinking=high -->
2026-08-24 16:52:54 +00:00
Jake Howell 614b5e2d22 chore(site): remove outdated featured empty-state images (#28454)
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.

## What

The images in `site/static/featured` were outdated and no longer
resembled the current UI, so this removes them along with the code that
referenced them:

- `workspaces.webp` — was the faded background image in the Workspaces
empty state (`WorkspacesEmpty.tsx`).
- `templates.webp` — was only referenced by a Storybook story
(`TableEmpty.stories.tsx`).
- `scheduling.webp` — had no references anywhere in the codebase.

### Cleanup alongside the removal

- Dropped the `image={defaultImage}` prop (and the now-dead
`defaultImage`) from all three `WorkspacesEmpty` states.
- Removed the `className="pb-0"` / `paddingBottom: 0` overrides that
only existed to seat the image flush against the bottom, restoring
balanced padding.
- Renamed the `TableEmpty` story `WithImageAndCta` → `WithCta` since it
no longer has an image.

## Old | New

| Surface | Old | New |
|---|---|---|
| Workspaces empty state | <img width="2306" height="1376"
alt="main-tableempty"
src="https://github.com/user-attachments/assets/1d3a5201-66ce-4f80-a2ea-5638221f95d0"
/> | <img width="2306" height="1376" alt="pr-tableempty"
src="https://github.com/user-attachments/assets/5a8fcc1d-b676-46e6-be54-12ff7f3489fd"
/> |
| Table empty state (templates) | <img width="2306" height="1376"
alt="main-workspaces-empty"
src="https://github.com/user-attachments/assets/7618ee1f-09d4-4398-8a1c-19362f63c80d"
/> | <img width="2306" height="1376" alt="pr-workspaces-empty"
src="https://github.com/user-attachments/assets/8a9c98a9-24b7-4c90-9959-f85dbc2daa9f"
/> |
| `scheduling.webp` | Unused | Deleted 🙂  |
<details>
<summary>Implementation notes / decision log</summary>

- Verified references with a repo-wide search for `/featured/` and each
`*.webp` filename. Only `WorkspacesEmpty.tsx` (real usage) and
`TableEmpty.stories.tsx` (Storybook-only) referenced them;
`scheduling.webp` was fully unused.
- `EmptyState`'s `image` prop is optional, so removing usages required
no component changes.
- Delete-only (per request): no replacement images committed. The empty
states now render as clean text/CTA layouts.
- "New" screenshots were captured from Storybook (a temporary
`WorkspacesEmpty` story was used only to render the states and was
**not** committed).
- Verified with `tsc --noEmit` (0 errors) and `biome check` (clean) on
the changed files.

</details>
2026-08-24 23:46:51 +07:00
Ethan eab5e28695 fix: move organization model overrides from Models to Coder Agents settings (#28473)
## Summary

Relocates the organization-scoped Coder Agents model override controls
from the **Defaults & overrides** tab under **AI Settings > Models**
into an **Organization settings** section on **AI Settings > Coder
Agents**. Models remains the home for reusable model definitions,
provider availability, default designation, and ACLs; Coder Agents now
owns all agent-specific model/reasoning override controls. The backend
organization scoping from #28442 is unchanged.

## Changes

- **Coder Agents page** is split into two sections:
- **Organization settings**: General subagent, Explore subagent, title
generation, compaction, and Advisor (behind the `chat-advisor`
experiment) model/reasoning overrides, with an organization picker when
more than one accessible organization exists.
- **Deployment settings**: personal override policy, virtual desktop
provider (behind its experiment), and Advisor usage limits, visible only
with `editDeploymentConfig`.
- **Permissions**: the page and its sidebar entry are accessible with
either `editDeploymentConfig` or organization chat model access.
Organization mutations require `editChatModelConfigs` on the selected
organization; otherwise controls render read-only. Organization
discovery, selected-organization permissions, and deployment queries
fail independently.
- **Models page** drops the Defaults & overrides tab and the
now-single-item tab UI. The old `DefaultsPage` components move to
`CoderAgentsPage/OrganizationAgentSettings*`.
- **Compatibility**: `/ai/settings/models/defaults` redirects to
`/ai/settings/coder-agents`, preserving `org` and other query
parameters.
- **Docs**: intentionally not updated here; a follow-up PR will update
`docs/ai-coder/agents/models.md` and
`docs/ai-coder/agents/platform-controls/advisor.md`.

## Testing

- Targeted unit tests (`ModelDefaultsRedirect`, `organizationModels`): 9
passing.
- Affected Storybook interaction tests (Coder Agents page/view,
organization agent settings, Models layout, AI settings sidebar): 41
passing.
- `pnpm check`, full `pnpm lint` (Biome, tsc, circular deps, React
compiler, knip), `pnpm format`.
2026-08-24 17:43:12 +01:00
Jake Howell d12cc2da21 fix(site): measure virtualized log row heights (#28251)
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.

Fixes coder/coder#25692. Closes coder/coder#25692. Supersedes
coder/coder#28250.

## Problem

`AgentLogs` virtualizes rows with a **fixed** `itemSize` (20px), so
react-window sets the total scroll height to `itemCount * 20`. Log
entries that contain newlines render taller than 20px, so the estimate
falls short: tall rows overlap the rows below them and the true bottom
sits past where `scrollToItem(..., "end")` can reach. The last lines
become unreachable.

## Fix

Switch to `VariableSizeList` and measure each row with a
`ResizeObserver`, feeding the real height back via `resetAfterIndex`.
The total scroll height now tracks the real content, so the bottom is
always reachable. `AgentRow` and `TaskPage` only change the ref type;
their existing autoscroll now lands correctly.

## Before / After

Dogfooded on a local instance with a workspace whose agent logs include
multi-line entries (241 entries, every 30th a 6-line block, plus a final
bottom-marker line). Same workspace, same scroll interaction, `main` vs
this branch:

<!-- linear:table-colwidths:266,266,266 -->
| | Before (`main`, `FixedSizeList`) | After (this PR,
`VariableSizeList`) |
| -- | -- | -- |
| **A multi-line entry** | Painted into a 20px slot, so it collides with
the rows above and below (text on top of text). | Rendered at full
height in its own space, neighbours no longer overlap. |
| | ![Before: multi-line entry overlapping neighbouring
rows](https://raw.githubusercontent.com/jakehwll/coder/devex-407-evidence/evidence/old-multiline.png)
| ![After: multi-line entry at full height, no
overlap](https://raw.githubusercontent.com/jakehwll/coder/devex-407-evidence/evidence/new-multiline.png)
|
| **Scrolled to the bottom** | Total height is short, so normal
scrolling stops early: the final marker line is clipped and unreachable.
| Height tracks real content: the final marker line is fully visible and
reachable. |
| | ![Before: bottom marker clipped and
unreachable](https://raw.githubusercontent.com/jakehwll/coder/devex-407-evidence/evidence/old-bottom.png)
| ![After: bottom marker fully
visible](https://raw.githubusercontent.com/jakehwll/coder/devex-407-evidence/evidence/new-bottom.png)
|

## Validation

The new `AgentLogs.test.tsx` renders rows measured taller (40px) than
the estimate and asserts the scroll area is sized from measured heights.
Run against the `main` implementation it fails, against this branch it
passes:

```
# main (FixedSizeList): scroll area = 5 rows * 20px
- Expected  200
+ Received  100      → FAIL

# this branch (VariableSizeList): scroll area = 5 rows * measured 40px
✓ AgentLogs.test.tsx (1 test)   → PASS
```

Also green: `tsc`, `biome check`, and the AgentRow (25) + TaskPage (46)
+ AgentLogs (3) Storybook interaction tests.

<details><summary>Dogfood details and decision log</summary>

**Dogfood setup** (SHA `1228dcfe8d`, `./scripts/develop.sh`): template
`lognl2` (a `coder_agent` on a `null_resource` so the agent
authenticates against a real build), workspace `lognl-demo2`. Posted 241
log entries via `PATCH /api/v2/workspaceagents/me/logs`, every 30th a
6-line `\n` entry plus a bottom marker; confirmed the 8 multi-line
entries persisted with embedded newlines. On the fixed build, at maximum
scroll the DevTools console reported `scrollTop 5204`, `scrollHeight
5460`, `clientHeight 256`, `remaining 0`, marker clipped `0px`; height
is self-consistent (`innerHeight 5428` + 16px top + 16px bottom padding
= `5460`).

**Decision log**

* coder/coder#28250 treated this as an autoscroll problem. Wrong: the
height calculation is the bug, so no scroll target can reach a bottom
placed out of range. Reverted.
* Only embedded newlines make a row exceed 20px; long lines do not wrap
(they overflow horizontally). A fixed constant can't cover a variable
line count, so measuring is the robust fix.
* `resetAfterIndex` can't run from a row's first-mount measurement
effect because the list ref is assigned after child rows commit; the
lowest changed index is recorded and flushed from a parent layout
effect.

</details>

Evidence images are hosted on the `devex-407-evidence` branch of the
author's fork (`jakehwll/coder`) to keep this repo's diff clean.
2026-08-24 23:25:57 +07:00
Jake Howell 18b37e64f5 chore(site/e2e): re-enable externalAuth suite (#26648)
> 🤖 This PR was written 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`
4. #26798 `fix(site/e2e): reset both providers in external auth hook`
5. #26648 `chore(site/e2e): re-enable externalAuth suite` ← this PR

Removes the `test.describe.skip` introduced in #17235 so the
externalAuth suite runs in CI again.

Splitting the five commits keeps the bisection signal clean if CI
behaviour changes after the un-skip.

Local verification of the full stack: `pnpm playwright:test
e2e/tests/externalAuth.spec.ts --repeat-each=20 --workers=1` → 61/61
passing in 4m48s.

Large whitespace churn in the diff is biome reformatting the body to its
natural indentation after the single-line `test.describe(...)` opener.
The only semantic change is removing `.skip`.

Refs https://linear.app/codercom/issue/DEVEX-413
Refs https://github.com/coder/internal/issues/356
2026-08-24 23:24:13 +07:00
Jake Howell cfc66b5de3 fix: reset both providers in external auth hook (#26798)
> 🤖 This PR was written 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`
4. #26798 `fix(site/e2e): reset both providers in external auth hook` ←
this PR
5. #26648 `chore(site/e2e): re-enable externalAuth suite`

Surfaced by stress-testing the leaf state of the stack locally with
`--repeat-each=N`. The suite passes on iter 1, then deterministically
fails the device test on every subsequent iteration:

| iter | device | web | workspace |
|---|---|---|---|
| 1 | ✓ 2.8s | ✓ 2.5s | ✓ 6.2s |
| 2 | **✘ 30.1s** | ✓ 2.6s | ✓ 6.2s |
| 3 | **✘ 30.1s** | ✓ ~2.5s | ✓ ~6s |
| … | … | … | … |

`resetExternalAuthKey` in `site/e2e/hooks.ts` only deletes the link for
`gitAuth.webProvider`, never `gitAuth.deviceProvider`. So:

```ts
path: `/api/v2/external-auth/${gitAuth.webProvider}?coder_session_token=${sessionCookie?.value}`,
```

Iter 1 of the device test creates a link for the `device` provider
against the test user. The cleanup hook between tests resets `web` but
not `device`. On iter 2 the page hits `/external-auth/device`,
`externalAuthProviderQuery.data.authenticated` is already `true`, the
page renders the authorized state immediately, and the test's
`sentPending.wait()` hangs forever because the mock token endpoint is
never polled. The whole test times out at 30s.

Fix: reset every provider the suite exercises. Refactored the per-link
delete into a Promise-returning helper so errors actually propagate; the
previous `throw` inside the response and `error` callbacks crashed the
worker rather than rejecting the call.

Doesn't affect CI (single run per PR, fresh `--ephemeral` coderd), but
it makes the suite robust to local stress testing and to anyone
re-running it in the same coderd process. Filed before the un-skip lands
so the un-skipped suite is repeatable from day one.

Local verification: `pnpm playwright:test e2e/tests/externalAuth.spec.ts
--repeat-each=20 --workers=1` → 61/61 passing in 4m48s. Same command
before this fix → 1 pass, then deterministic device-test timeouts.

Refs https://linear.app/codercom/issue/DEVEX-413

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

This is the fourth distinct root cause that piled up while the suite was
skipped, and it's orthogonal to the other three. #26575 fixes the
listener leak, #26793 fixes the 404 hook contract drift, #26795 fixes
the dropped invalidation, and this PR fixes the per-test cleanup so the
suite is actually repeatable. #26648 then just flips `.skip`.

</details>
2026-08-24 23:12:52 +07:00
Michael Suchacz b6d7653231 fix: apply MCP server selection when editing a chat message (#28471)
## Problem

Editing a user message in an Agent chat silently dropped the MCP server
selection. The composer renders the MCP picker in edit mode and toggles
update local client state, so the picker displayed the new selection,
but the edit request omitted `mcp_server_ids` at every layer (frontend
request builder, `codersdk.EditChatMessageRequest`, the `PATCH
/chats/{chat}/messages/{message}` handler, and `chatd.EditMessage`). The
chat's persisted selection never changed and the regenerated turn ran
without the newly enabled MCP tools. Two dogfood users hit this within
hours; there is no error anywhere and the UI shows the opposite of the
server state.

## Changes

- `codersdk`: add `MCPServerIDs *[]uuid.UUID` to
`EditChatMessageRequest`, mirroring `CreateChatMessageRequest` (nil
preserves the current selection).
- `chatd`: extract the send path's MCP update block into one shared
`applyRequestedMCPServerIDs` helper (explore-subagent snapshot
immutability guard plus Force On enforcement, Cure53 CDM-02-010) and
call it from both `SendMessage` and `EditMessage`, so enforcement cannot
drift between the two paths.
- `coderd`: extract the send handler's request validation (dedupe,
enabled-in-organization check, persisted-ID exemption) into
`normalizeRequestedChatMCPServerIDs` and wire it into the edit handler,
which now threads the selection into `EditMessageOptions`.
- Frontend: the edit request now includes `mcp_server_ids:
[...effectiveMCPServerIds]`, exactly like the send path, making the
picker's displayed state real.
- `make gen` artifacts (swagger, API docs, `typesGenerated.ts`).

## Tests

Each layer is covered and was proven with independent red toggles
(removing one layer's wiring fails only that layer's tests):

- chatd (`TestEditMessage_MCPServerIDs`): edit applies a provided
selection, nil preserves it, an emptied list cannot remove a `force_on`
server, and explore subagent chats keep the spawn-time snapshot.
- API (`TestPatchChatMessage/MCPServerIDsApplied`,
`MCPServerIDsInvalidRejected`): persistence via the endpoint, omission
preserves, unknown IDs get the same 400 as the send path.
- Storybook (`EditAppliesMCPServerSelection`): toggling a server on
during an edit puts it in the edit request payload.

Note: the `AgentChatPage.stories.tsx` story "Queued For Capacity After
Polling" fails locally on current main as well (verified against the
main baseline with this branch's changes reverted); it is unrelated to
this diff.

Remote dogfood UAT ran on the exact head and passed, including proof
that the regenerated turn actually gains the newly enabled MCP server's
tools.

> 🤖 Xum acted on Mike's (@ibetitsmike) behalf. • Model:
`anthropic:claude-fable-5`
<!-- xum-attribution: model=anthropic:claude-fable-5 -->
2026-08-24 17:39:52 +02:00
Jake Howell 990a7c64f3 fix(site): show "Never" for unused tokens (#28453) 2026-08-24 20:07:28 +07:00
Jeremy Ruppel 3c89b09734 fix(site/src/pages/TemplateBuilder): validate customizations form before submit (#28081) 2026-08-24 08:48:24 -04:00
Ethan 9b5f47eeb7 fix(site/src): repair failing Storybook stories (#28462)
## Summary

Repair the Storybook interaction and Pixel failures currently present on
`main`. The failures came from organization-scoped model changes leaving
stories with incomplete providers and query fixtures, product copy and
route changes leaving stale assertions, and several interaction tests
depending on implementation details or teardown timing.

## Broken stories and fixes

### Organization-scoped chat models

PR #27960 introduced organization-scoped chat models and the following
regressions:

- `OrganizationModelsLayout / Switch Organization Preserves Auxiliary
Parameters`, `Invalid Requested Organization Falls Back To Default`,
`Invalid Requested Organization Denies Add`, `Duplicate Display Names
Are Disambiguated`, and `No Readable Organization Is Not Found`: the
stories only populated permission query keys for individual
organizations, while the accessible-organization lookup requests
authorization for all visible organization IDs together. The unmatched
`/api/v2/authcheck` request returned a Storybook proxy 502, so the
layout rendered an error instead of the intended picker, fallback,
denied, disambiguation, or not-found state. Add fixtures for the
combined organization-permission keys, preserve the intentionally denied
permission map, and return an explicit empty authorization result for
the no-readable-organization case.
- `ModelFormProviderConfig / Provider Config Open AI`, `Provider Config
Anthropic`, and `Provider Config Open AI Web Search`: `ModelForm` began
consuming `OrganizationModelsContext`, but these stories were not
wrapped in its provider and rendered the router error boundary. Add the
same organization-model context decorator used by the sibling model form
stories.
- `AgentChatPage / Queued For Capacity After Polling`: the story
retained a manually assembled chat-and-messages fixture after the page
gained organization-model, provider, workspace, prompt, diff, chat-list,
and authorization dependencies. Those missing queries prevented the
polling request from being reached. Replace the partial fixture list
with the shared `buildQueries()` setup.
- `DashboardLayout / Custom Organization Role Can Open Models`,
`DashboardLayout / ACL Readable Member Can Open Models`, and `NavbarView
/ For Member With Model Access`: these stories also landed in #27960 and
inherited Pixel's tablet-and-desktop matrix, but their play functions
exercise the desktop `Models` link. Pixel's 744px tablet viewport
renders that link inside the closed mobile menu, so the desktop query
always failed there. Restrict these authorization-to-navigation stories
to the desktop matrix; mobile Models navigation remains covered by the
dedicated `MobileMenu` story.

### Premium copy and navigation

- `DeploymentSidebarView / Premium Tab Visible` and `Premium Tab
Hidden`: PR #28226 renamed the production navigation item from `Premium`
to `Trial Upgrade`, but added stories that still queried the old name.
Update both the positive and negative assertions so the hidden-state
story cannot pass while the real CTA is present.
- `PremiumPageView / No License`: PR #28226 changed the production
heading to `Start an unlimited 30-day Coder trial` while the story
asserted the previous Premium wording. Update the accessible heading
assertion to the rendered copy.
- `AgentChatPageView / Queued For Capacity Community Admin`: PR #28437
intentionally moved the trial CTA from `https://coder.com/trial` to the
internal `/deployment/premium` route, leaving the story's href assertion
stale. Update the expected route while retaining the link-name and
callout checks.

### MCP server refetch behavior

- `AgentCreateForm / MCP Servers Error Shows Alert And Disables Send`
and `MCP Servers Refetch Error Keeps Send Enabled`: the MCP coverage was
introduced in #27942. PR #28442 later added a second unconditional MCP
`ErrorAlert`, so a background refetch error appeared even when cached
MCP data remained usable. The refetch story also called
`refetchQueries()` without a key, which began refetching unrelated
active model queries as the form's query surface expanded and produced
unmatched API failures. Remove the duplicate unconditional alert,
refetch only the organization's MCP query, and use semantic alert and
heading assertions. Initial-load failures still disable Send, while
background failures with cached data keep Send enabled without replacing
the form with an error.

### Interaction and teardown stability

- `IconField / Open Picker`: PR #27674 changed this story to wait for
the `em-emoji-picker` custom element. That implementation-specific query
races the lazy-loaded picker chunk and violates the component's
observable contract. Keep the button state assertion and wait for the
visible dialog instead.
- `AgentChatPage / Slash Compact Command Submits` and `Slash Compact
Yields To Personal Skill`: the command story added in #27081 waited on
cmdk's `Commands` group heading, which is accessibility-hidden, while
the skill variant queried raw implementation text. Menu placement and
visibility are asynchronous, especially after the positioning changes in
#28411. Wait for the visible selectable options by role before pressing
Enter.
- `AgentChatPageView / Terminal Focus On Tab Switch`: the focus coverage
added in #24677 exposed an xterm teardown race rather than a product
navigation regression. xterm queues its initial viewport
synchronization, but Storybook could synchronously dispose the terminal
first, leaving the queued callback to read a cleared renderer and report
an unhandled error. Clear React state immediately, defer xterm disposal
by one timer turn, query the labeled terminal textbox semantically, and
remove the unnecessary empty WebSocket message fixture.
2026-08-24 13:46:15 +01:00
Cian Johnston 042421694f fix: surface duplicate AI provider hostname as warning, not error (#28208)
Two live `ai_providers` rows can share the same `base_url` hostname.
This is an intended configuration (AIGOV-97): the direct routing path
(`/api/v2/ai-gateway/{name}/...`) routes by provider name and fully
supports it. Only the MITM proxy path (`aibridgeproxyd`) cannot
disambiguate, because its `nameByHost` map is 1:1.

`classifyProviderRow` marked the second provider as
`ProviderStatusError`, which surfaced as a misleading log warning and a
`status=error` Prometheus label. The provider is not actually broken; it
is fully routable via the direct path. The error was only visible in
server logs and Prometheus, not in the API or UI.

This change:

- Extracts `BaseURLHostname` into `coderd/aibridged` and eliminates
three duplicate implementations (proxy classifier, chatprovider, and the
API layer now uses it for status checks).
- Downgrades the proxy classifier from `ProviderStatusError` to a new
`ProviderStatusProxyExcluded` status. The provider is not broken; it is
just not reachable via the MITM proxy path.
- Adds `Status *AIProviderStatus` to `codersdk.AIProvider` with
`Warnings []string`, matching the `WorkspaceProxy` precedent.
- Populates warnings on create, update, GET, and list when a provider's
hostname collides with another enabled provider.
- Adds an integration test proving that a duplicate provider is routable
via the direct path but not via the proxy path.

Refs AIGOV-596

Generated by Coder Agents



https://github.com/user-attachments/assets/bf323837-5121-4d1b-ab19-593f1d32374e
2026-08-24 13:27:22 +01:00
Atif Ali 54d4ae9aca fix: detect Xum client in aibridge after Mux rename (#28448) 2026-08-24 16:20:57 +05:00
Ethan c275327fb7 feat: audit operational agent settings (#28369)
Depends on #28394.

Audit changes to seven operational agent settings through a typed
`chat_operational_settings` resource. Each handler serializes baseline
capture and mutation with a per-setting advisory lock, keeps both
operations in one transaction, and records field-specific diffs.

Effective no-op requests now skip both the database upsert and the
success audit, including absent rows whose effective value is already
the default. The change also retains the allowlisted raw `site_configs`
query, updates generated audit and API surfaces, adds failure and
concurrency coverage, and reuses the existing AuditPage resource-filter
story pattern.

Relates to CODAGT-720.
2026-08-24 11:53:47 +01:00
Michael SuchaczandEthan Dickson 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>
2026-08-24 11:20:26 +01:00
Ethan 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.
2026-08-24 11:20:26 +01:00
Ethan 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._
2026-08-24 11:20:25 +01:00
Ethan 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._
2026-08-24 11:20:25 +01:00
Ethan 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._
2026-08-24 09:43:45 +00:00
Ethan 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._
2026-08-24 09:43:45 +00:00
Jaayden Halko 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>
2026-08-22 14:01:46 +07:00
Michael Suchacz 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.
2026-08-21 20:35:40 +02:00
Jeremy Ruppel 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._
2026-08-21 12:58:12 -04:00
Matt Vollmer 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.
2026-08-21 16:55:26 +00:00
david-fraley cb52f5339d feat: show a premium paywall on the external auth settings page (#28435) 2026-08-21 11:45:38 -05:00
david-fraley 9ce42293b0 fix(site/src): use the browser-only bullets from the copy doc (#28434) 2026-08-21 11:24:18 -05:00
david-fraley b8fcdbd32f fix(site/src): sync paywall copy with the latest copy doc (#28436) 2026-08-21 11:18:08 -05:00
david-fraley 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
2026-08-21 10:31:01 -04:00
Samuel VolinandMatt Vollmer 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>
2026-08-21 09:54:54 -04:00
Michael Suchacz 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 -->
2026-08-21 08:19:17 +02:00
Michael Suchacz 05fcd06979 fix(site): auto-select MCP server after successful OAuth (#28155) 2026-08-21 05:43:54 +02:00
TJ 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.
2026-08-20 23:54:53 +00:00
TJ 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._
2026-08-20 23:08:51 +00:00
TJ 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.
2026-08-20 23:08:00 +00:00
TJ 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.
2026-08-20 22:32:41 +00:00
david-fraley e7eea0f06f feat: update premium paywall copy and link each page to its feature docs (#28339) 2026-08-20 17:06:21 -05:00
TJ 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.
2026-08-20 21:55:59 +00:00
Michael Suchacz 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 -->
2026-08-20 22:35:35 +02:00
Ben PotterandMichael Suchacz 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>
2026-08-20 21:54:59 +02:00
Michael Suchacz 9d6ef1e701 docs: streamline agent instructions (#28243)
## Summary

Streamlines the agent instruction files, cutting per-session token
overhead by roughly 65%.

The core design change: `AGENTS.md` no longer `@`-imports WORKFLOWS.md,
package.json, the eight detailed guides, or `AGENTS.local.md` into every
session. Guidance moves from guaranteed-in-context to load-on-demand
through a routing table, betting that one-line guardrail summaries plus
links are enough to make agents pull the right doc when a task needs it.

- `AGENTS.md` (251 -> 80 lines): a routing table to the canonical docs
in `.claude/docs/`, the essential command table, and one-line guardrails
for the architectural footguns (SQLC workflow plus audit table, OAuth2
error conformance, chatd architecture-doc rule, Swagger annotations,
`InTx` discipline, test concurrency, workflow `permissions: {}`,
mandatory git hooks, commit/PR title format).
- `site/AGENTS.md` (364 -> 89 lines): keeps the FE1-FE10 contract
summary, TypeScript LSP navigation, pnpm command table, React Compiler
boundaries, accessibility rules, and the pre-PR `frontend-review`
requirement. Drops the generic React tutorial sections that restate
framework documentation.
- The persona and relationship prose ("Our relationship", foundational
rules) is deliberately removed. The behavioral gates worth keeping for
every agent survive as one-liners: get explicit permission before
excepting a rule, discuss architectural decisions before implementing,
and answer questions instead of jumping to implementation.
- Unique operational content is relocated rather than deleted:
Playwright failure-artifact paths and the CI `playwright-artifacts-`
naming convention move to `.claude/docs/AGENT_FAILURES.md`; the
`React.useId()` rule, the `key={String(booleanState)}` remount footgun,
date-as-prop Storybook determinism, and the suppression-comment rule
(updated from `eslint-disable` to `biome-ignore` to match current
tooling) move to `.claude/docs/FRONTEND_PATTERNS.md`.

Content reflects current main: the chatd rule says agents leave TODOs
and the human PR author writes the architecture doc, and MUI/Emotion are
described as removed, not deprecated.

Note for Claude Code users: `CLAUDE.md` symlinks to `AGENTS.md`, so
detailed workflow docs (hook internals, the new-feature checklist,
`AGENTS.local.md`) are no longer auto-loaded into every session; agents
must follow the links.

## Validation

- `pnpm exec markdownlint-cli2` on all four touched files: 0 errors
- `pnpm exec markdown-table-formatter --check` on all four touched
files: pass
- `make lint/emdash`: pass
- `make lint/agents`: pass
- All relative links verified to resolve

> Mux created this PR and description on behalf of Mike.
2026-08-20 21:05:08 +02:00
Michael Suchacz 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 -->
2026-08-20 20:51:59 +02:00
Danielle Maywood c940284e18 fix(site): freeze motion animations during Pixel captures (#28396) 2026-08-20 17:43:32 +00:00
Michael Suchacz 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.
2026-08-20 19:24:10 +02:00
TJ 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._
2026-08-20 16:42:35 +00:00
TJ 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._
2026-08-20 09:35:42 -07:00
TJandCoder Agents 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>
2026-08-20 15:56:36 +00:00
Jake Howell 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>
2026-08-20 22:22:32 +07:00
Tyler 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.
2026-08-20 10:17:03 -05:00
Jake Howell 47ffa87fa5 fix: <BuildingTemplateLoader /> icon sizes (#27733) 2026-08-20 21:44:27 +07:00
Michael Suchacz 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 -->
2026-08-20 16:36:57 +02:00
Susana Ferreira 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.
2026-08-20 14:51:27 +01:00