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 -->
Phase 2 of the Scalable Approach for Adding New IDE Session Types RFC,
tracked in #27410.
Adds a session_counts map to Stats on the Agent API, so an agent can report
a count per app name instead of the four fixed fields, which are now marked
deprecated. Bumps the API to v2.11.
- coderd normalizes names on ingest, caps distinct entries, and sums the
overflow under unknown, so an arbitrary client name cannot blow up
cardinality.
- Agents predating v2.11 set the fixed fields instead, never both, and the
ingest path falls back to them.
Nothing sends session_counts yet. The agent starts populating it in #28338.
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.
Phase 1 of the Scalable Approach for Adding New IDE Session Types RFC,
tracked in #27410.
Gives agent session accounting one canonical vocabulary for app names, so
later work can report a count per app without the agent and the server
disagreeing about what an app is called.
- Adds codersdk/appname.go: NormalizeAppName for storage and lookup, and
AppFamilyName for the places that need a bounded value, such as a
Prometheus label or the Connection_Type enum.
- agentssh.Server keeps session counters in a map keyed by normalized name
instead of three fixed atomics, dropping entries at zero.
- Drops MagicSessionType. Sessions carry plain app names, and unrecognized
ones flow through as themselves rather than collapsing to unknown.
Wire behavior is unchanged: the agent still reports the four session_count_*
fields over Agent API 2.10.
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._
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.
`TestUserSecretAuditDiffRedaction` flakes when create and update audit
rows receive equal timestamps and `GetAuditLogsOffset` returns the tied
rows in either order.
Identify audit rows by action, or by diff content when both actions are
writes, instead of relying on slice position. Fixescoder/internal#1625.
Reviewed and updated by Coder Agents on behalf of @dylanhuff-at-coder.
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.
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.
Centralizes every LLM client build in chatd behind one resolver and
makes all model calls behave the same, instead of preserving per-flow
quirks.
## Why
`chatd` built LLM clients in five places (standard turn, compaction
override, quickgen, title override, advisor), each repeating config
parsing, route resolution, client construction, and debug wrapping with
slightly different behavior. An earlier iteration of this PR centralized
construction but preserved every historical quirk behind
purpose-specific spec constructors and policy flags. Review concluded
the quirks themselves were the problem.
## What changed
- `Server.resolveModelCall` (`modelcall.go`) is the single pipeline from
a `modelCallSpec` to a ready client plus call metadata. Callers build
spec literals inline; the only spec fields are data: model source (chat
config, explicit config row, or fixed provider/model pair for computer
use), requested reasoning effort, chatd-scoped routing, and build
options.
- Uniform behavior for every call:
- Provider options are always derived. Whole-chat summaries, turn status
labels, and chat-model compaction summaries previously sent none; they
now follow the config like everything else.
- `MaxOutputTokens` always defaults to 32k when the config leaves it
unset. The compaction summary call nulls the cap after copying the
template because it is non-streaming and the Anthropic SDK rejects
non-streaming requests whose cap implies a completion over ten minutes.
- Debug recording always wraps the client when chat debug is enabled,
labeled with the resolved provider/model. The debug-rebuild paths
(`manualTitleDebugSpec`, `quickgenDebugSpec`) are deleted; debug helpers
now only create and finalize run records, since run association flows
through the generate context, not the model.
- Clients are always constructed with the configured model string and
unmutated route, so AI Gateway validation sees the name exactly as
configured. The resolved identity feeds metadata, logs, and debug labels
only.
- `resolvedModelCall.newCall` / `newObjectCall` are the only production
constructors of `fantasy.Call` / `fantasy.ObjectCall`; object calls now
carry the config's sampling parameters.
- Net: -240 lines.
## Validation
- Full `./coderd/x/chatd/...` suite green (PostgreSQL-backed).
- Red-green toggles verified for the three key unifications (option
derivation, token default, summary cap removal).
- ARCHITECTURE.md: prose the refactor made stale is deleted; no new
resolver documentation is added.
> Mux acted on Mike's behalf for this PR.
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 -->
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>
Third in the stack, split out of #27873. This is where dynamic client
registration first produces a public client.
- A registration requesting token_endpoint_auth_method `none` issues no
secret and persists the client_type derived in #28043. The response
omits client_secret entirely, per RFC 7591 §3.2.1.
- Discovery still advertises only the two secret-based methods. `none`
is withheld until the token endpoint honors it, so a conforming client
is not told to attempt an exchange that would be rejected.
- The app and its secret are written in one transaction. They were two
independent inserts, so a failed second one left a committed app that
could never authenticate while still holding a registration access
token. Pre-existing, but a public client's missing secret row makes the
orphaned case unspottable by inspection.
- registration_client_uri uses url.JoinPath, fixing a trailing-slash
access URL producing //oauth2/clients/{id}.
- Docs state both limits above, so the page does not describe a flow
that returns 400 until #28047 lands.
A client registered here cannot obtain a token yet; #28047 adds that.
Dynamic client registration is off by default, so none of this is
user-visible until then.
Refs
https://linear.app/codercom/issue/ENG-3029/oauth2-support-public-client
Refactors the mantle SigV4 signing middleware into a new shared
`intercept/bedrocksig` package.
Prepares for OpenAI protocol support on the mantle provider (follow-up
PR).
---
Generated by Coder Agents on behalf of @johnstcn.
## What
Two small docs fixes, split out of the offlinedocs Fumadocs migration
(#27390) so that PR stays a focused tooling change and these land on
their own review track.
- `docs/about/contributing/documentation.md`: the Vale style-guide link
pointed at `docs/.style/style-guide/`, which is excluded from the
published docs corpus, so the relative link 404s on the live site.
Repointed to an absolute github.com URL.
- `docs/ai-coder/ai-gateway/monitoring.md`: lifted the "Possible client
values" `<details>` block out of the list item so it renders as a
top-level accordion, and restored a dropped blockquote continuation
marker (`>`).
## Why
Both are genuine docs bugs on their own:
- The `.style/` link is broken on the live site today, because that
directory is intentionally excluded from the published corpus.
- The `<details>` nested inside a list item renders inconsistently;
lifting it to a sibling block is portable across renderers.
They are also prerequisites for the offlinedocs Fumadocs migration,
whose stricter build-time sync hard-fails on unmapped inter-doc links
and only converts a `<details>` to an accordion when it is a list
sibling. Landing them here first lets the migration PR drop these two
files on its next rebase.
## Validation
- `make fmt/markdown` and `make lint/markdown`: clean (via
pre-commit-light).
- Docs-only change; no CI or build config touched.
> This PR was created with AI assistance (Coder Agents).
## 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.
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 -->
Follow-up to the review discussion on the price book refresh automation:
https://github.com/coder/coder/pull/28146#discussion_r3806439966
## Problem
The price seed row struct was duplicated three times, so a JSON tag
change had to be repeated in each copy:
- `scripts/aibridgepricesgen.priceRow`
- `scripts/aibridgepricesdiff.priceRow`
- `coderd/aibridge/prices.seedRow`
The `(provider, model)` key and its parsing were duplicated too.
## Change
Add `coderd/aibridge/prices/pricebook` holding the shared contract:
`Row`, `Key`, `Row.Key()`, and `Parse`. The package depends only on the
standard library, so the scripts can import it without pulling in the
database layer that `coderd/aibridge/prices` brings.
Consumer-specific behaviour stays where it is: upstream conversion and
validation in the generator, comparison and Markdown rendering in the
diff tool, DB seeding and runtime lookup in the seeder.
The batch SQL upsert still extracts JSON fields by name, so it remains a
separate contract boundary. The note about keeping it in sync now lives
on the shared `Row`.
No generated artifacts change; the serialized schema is identical.
---
<sub>Opened by Coder Agents on behalf of @evgeniy-scherbina.</sub>
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.
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._
## 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._
The docs named the `ai_model_price:update` permission but never said
which role holds it, and nothing anywhere explained that a price is
keyed by provider type and model, so every configured provider of that
type shares it. This names the Owner role, documents the keying and why
`openai-compat` cannot be priced, and repeats both in the CLI's
`--provider` descriptions and `update` help.
> [!NOTE]
> Initially generated by Claude Opus 5, modified and reviewed by
@ssncferreira
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
Refreshes the six stale screenshots on the
[Workspaces](https://coder.com/docs/user-guides/workspace-management)
page (`docs/user-guides/workspace-management.md`) with the current Coder
UI, recreating equivalent dummy data (matching template and workspace
names) so the images stay recognizable.
Linear: DOCS-55
## Updated images
| Image | Section |
|-------|---------|
| `creating-workspace-ui.png` | Creating workspaces |
| `workspace-update.png` | Updating workspaces |
| `workspace-automatic-updates.png` | Automatic updates |
| `user-guides/workspace-bulk-actions.png` | Bulk operations |
| `schedule.png` | Starting and stopping |
| `templates/template-variables.png` | Repairing workspaces |
Prose is unchanged; this is a screenshot-only refresh.
## Notes / limitations
The originals came from production-like deployments with
AWS/Azure/Kubernetes templates, multiple users, and aged data. These
were reproduced on a local `scripts/develop.sh` (Docker) instance, so:
- Template/workspace names, states (Running/Stopped/Failed), the
"Selected 3 of 5" bulk selection,
`use_kubeconfig=True`/`namespace=default`, and the auto-update =
`Always` setting all match.
- Instance-derived values cannot match: "used by" counts show `0
developers`, build times show `Unknown`, the connection-latency pill has
no value, and the workspace name field is disabled by the template.
<details>
<summary>How these were produced (decision log)</summary>
Followed the `dogfood` skill:
1. Started a local instance with `./scripts/develop.sh`
(`v2.36.0-devel`, matching the docs version).
2. Created Docker-backed templates named to match the originals:
`data-warehouse`, `analytics`, `api-services`, `frontend-web`,
`pod-share-jupyter`, `aws-linux-ephemeral`, `azure-aks-pod`, plus
`kubernetes2` ("Develop in Kubernetes"). The JetBrains module was
dropped from the Docker template to avoid a plan-time error;
`kubernetes2` uses a Coder-only variant declaring the real `namespace`
and `use_kubeconfig` variables so it imports with `use_kubeconfig=true`.
3. Created workspaces: `admin/my-frontend` (outdated → update available,
with a schedule), `admin/dev` (auto-update = Always), and
`kirby/{data-science, aws-large, azure-space, ephem-demo (Failed), kirbs
(Stopped)}`.
4. Captured each page with a headless Playwright script, hiding dev-only
chrome (the sticky deployment banner, the navbar `::before` hazard
stripe, and the `-devel` version chip) and cropping settings pages to
the form. Images are 2x for crispness.
5. Each screenshot was visually verified against the original for
content parity.
</details>
Split out of #28045
([PLAT-479](https://linear.app/codercom/issue/PLAT-479)) so the coverage
rules can be reviewed on their own. First of three. No behavior change:
both functions are added without production callers, and the OAuth2
authorize endpoint consumes them in the next PR.
**ScopesCover(allowed, requested)** reports whether every permission the
request grants is also granted by the allowlist. It expands both sides
and compares permissions rather than names, so coder:workspaces.access
covers workspace:read without ever naming it.
The fail-closed rules are the part worth reviewing:
- Coverage models site-level grants only. A scope carrying org or user
permissions, a negative permission, or a narrowed allow list is refused
on either side rather than compared on the part that is modeled.
Answering "covered" from the fraction that was read would report
authority that was never examined.
- An unknown name on either side is an error, not a false, since a
caller cannot tell "not covered" apart from "could not decide".
- A wildcard request is covered only by a wildcard grant. Enumerating
every workspace action that exists today does not cover workspace:*,
because the wildcard also authorizes the actions added tomorrow.
**CanonicalScopeName** maps the aliases IsExternalScope accepts, all and
application_connect, onto the api_key_scope enum spellings.
IsExternalScope answers whether a name may be requested, not how it is
spelled once persisted, so anything storing a validated name has to
canonicalize in between. Both sides of ScopesCover must already be
canonical, which its parameter names restate at every call site.
Covered by 17 coverage cases, guard cases that drive each unmodeled
shape through both sides of the comparison, and an exhaustive sweep over
the external catalog: coder:all covers all of it, and every catalog name
covers itself.
Stack: this PR, then negotiate and persist the scope, then the consent
page and invalid_scope redirect.
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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>
> 🤖 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>
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.
Playwright's `--with-deps` flag performs a live apt refresh and install
in the E2E and Storybook jobs, even though the runner images already
contain the required system libraries.
Install only Playwright's pinned Chromium build in CI so apt mirror
failures cannot block browser setup.
Closes DEVEX-792
## 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 -->
Points README readers to the right places and cuts redundancy.
## Links
- Dev Containers now links to the docs, so people land on setup steps
instead of the envbuilder repo
- Why Coder now links to the docs
(https://coder.com/docs/about#why-coder) instead of the marketing page
- Removed JetBrains Gateway Plugin (retired)
- Removed the community Terraform provisioning link (outdated)
- Added AI Gateway to the docs section, since it's not discoverable
otherwise
- Added Coding Agents, pointing to the registry modules for running
Claude Code, Codex, and OpenCode isolated in Coder workspaces
- Added the Terraform provider (coderd) for declaratively managing Coder
deployment configuration as code
- Added the Coder Agents Chat Action under community integrations
- Moved Coder Discord to the top of the community list and clarified
it's where to get support and hear about new product updates first
## Structure
- Merged the redundant Quickstart and Install sections into one install
→ try locally → production flow, with a note up top pointing to the
install guides. `coder server` now appears once per purpose instead of
three times
- Windows install now says "latest binary or installer" and links to
GitHub Releases
- Support section now spells out what goes where: issues for bugs and
feature requests, Discord and GitHub Discussions for community support
and showcasing what you built, and dedicated support included in Coder
Premium (coder.com/pricing)
🤖 Generated with Coder Agents on behalf of @bpmct
## Summary
Fixed a bug where MCP tools with an empty `properties` schema had their
tool definitions sent to OpenAI with `"properties": null`, causing
OpenAI to reject the request.
## Problem
When an MCP server reports a tool with an empty input schema, e.g.:
```json
{
"type": "object",
"properties": {},
"required": []
}
```
Coder was forwarding the tool definition to OpenAI as:
```json
{
"type": "object",
"properties": null
}
```
OpenAI's function-calling API rejects `properties: null`, making these
tools unusable with OpenAI models.
## Root cause
The empty `properties` object collapsed to Go `nil` at two layers:
1. `agent/x/agentmcp/manager.go` `toolInputSchemaMap` stripped the
present-but-empty `"properties": {}` (guarded by `len(properties) > 0`),
so the wire copy lost it entirely.
2. `coderd/x/chatd/chatloop/chatloop.go` `buildToolDefinitions` embedded
`info.Parameters` directly into the input schema. When nil,
`encoding/json` marshals it as `null`.
## Fix
- `buildToolDefinitions` now substitutes an empty object when
`Parameters` is nil, mirroring the existing `required` guard (the sink,
protects all tool sources).
- `toolInputSchemaMap` now preserves an empty `properties` object
instead of dropping it (the upstream producer).
## Tests
Added regression tests (failing before, passing after) covering both
layers and asserting the schema serializes to
`{"type":"object","properties":{}}`:
- `TestBuildToolDefinitionsNilPropertiesBecomesEmptyObject`
- `TestToolInputSchemaMapPreservesEmptyProperties`
-
`TestWorkspaceMCPToolInfosFromResources/EmptyPropertiesYieldsEmptySchema`
---
*This PR was generated by Coder Agents.*
## 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.
## Problem
AI budget admin notifications tell an admin that a user crossed a budget
threshold, but give them nothing to act on: no link to the user's
budget, and no pointer to the docs.
## Changes
- Add a **Manage user AI budget** action to both admin templates,
linking to the effective group's members page filtered to the affected
user, where the per-user override is set.
- Link to the AI budget documentation from both notification bodies.
- Say when the block lifts in the limit-reached notification ("blocked
until `{{period_end}}`").
- Add an `organization_name` label, needed to build the members page
URL.
- Grant the AI Gateway daemon subject read access to organizations,
which the label lookup requires.
- Add an authorization test for the notification path, which also covers
the existing group read grant.
<details>
<summary>Screenshots</summary>
**Admin Notifications in Coder UI**
<img width="456" height="695" alt="admin_notifications"
src="https://github.com/user-attachments/assets/a579f91c-55cc-497c-87f4-235f15493725"
/>
**Admin Notification in Email**
<img width="660" height="399" alt="admin_notifications_email"
src="https://github.com/user-attachments/assets/39b876fb-f512-41e5-997f-a5269cd26ddc"
/>
</details>
Related to internal Slack threads:
* https://codercom.slack.com/archives/C096PFVBZKN/p1786051630688569
* https://codercom.slack.com/archives/C04M9CNGHGF/p1786453662306149
> [!NOTE]
> Initially generated by Claude Opus 5, modified and reviewed by
@ssncferreira
## 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.
The OAuth GitHub default provider test treated the config URL as proof
that the restarted HTTP server was ready. On macOS, the first SDK
request could reach the listener before it was ready to serve and fail
with `readLoopPeekFailLocked`.
Poll `AuthMethods` through the Coder SDK with the existing bounded retry
helper before asserting its response, both before and after the restart.
Closes
https://linear.app/codercom/issue/ENG-3257/flake-testserveroauth2githubdefaultproviderallowedorg
<sub>Generated by Coder Agents.</sub>
## 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
## Description
The experimental CLI could only price models the price book does not cover, so an operator with a negotiated rate for a model Coder already prices had no way to record it. The restriction existed because a single row served both the price book and prices set through the API, and the startup seeder would overwrite an override on the next restart.
Each source now keeps its own row (https://github.com/coder/coder/pull/28204), so that no longer applies. This removes the restriction: a price set through the CLI takes effect over the price book for any model, and the book stays underneath it.
## Changes
- Remove the validation that rejected models the price book covers.
- Delete `prices.IsDefaultPriced` and the index behind it, which existed only for that check.
- Generalize the `coder exp ai-model-prices update` help.
- Document that a custom price overrides the price book and does not follow its updates.
- Rework the spend approximation callout around what a custom price can and cannot correct.
Closes https://linear.app/codercom/issue/AIGOV-594
> [!NOTE]
> Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira
## Description
Prices in `ai_model_prices` come from two places: the price book embedded in each Coder release, and prices set through the experimental CLI (https://github.com/coder/coder/pull/27926). Until now, they were indistinguishable, so the startup seeder re-applied the book over every row, and a price someone set would silently revert on the next restart.
This adds a `source` column with values `default` and `custom`, and keeps a row for each. Readers resolve a model to its custom price when one exists and fall back to the book, including the cost path that prices an interception.
Keeping both rows means the price book stays current underneath an override, so a future delete can restore it.
Note: the endpoint still only accepts models the price book does not cover. Overriding a price the book already carries is handled by the follow-up issue https://linear.app/codercom/issue/AIGOV-594.
## Changes
- Add a migration for the `ai_model_price_source` enum and a `source` column, backfilled to `default` and then `NOT NULL`. `source` joins the primary key, so a model can hold one row per source.
- Take a `source` parameter on `UpsertAIModelPrices`. Each source keeps its own row, so the price book and a custom price never overwrite each other.
- Resolve to the custom price in `GetAIModelPriceByProviderModel` and `GetAIModelPrices`, so both the interception cost snapshot and the list endpoint use it.
- Seed the price book as `default`, and write prices set through the endpoint as `custom`.
Closes https://linear.app/codercom/issue/AIGOV-592
> [!NOTE]
> Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira
Removes the six task-management tools from the MCP tool surface in
`codersdk/toolsdk`:
- `coder_create_task`
- `coder_delete_task`
- `coder_list_tasks`
- `coder_get_task_status`
- `coder_send_task_input`
- `coder_get_task_logs`
Both the coderd-hosted MCP server (`coderd/mcp`) and the CLI `coder exp
mcp server` derive their tool list from `toolsdk.All`, so removing the
registry entries removes the tools from both servers with no server-side
changes.
The MCP server documentation (`docs/ai-coder/mcp-server.md`) is updated
to drop the task-management entry from the tool overview.
## What stays
- `coder_report_task` is intentionally kept: it is the agent-side
status-reporting mechanism (`WithTaskReporter`), already excluded from
the hosted MCP server, and required by `coder exp mcp server`, Claude
Code configuration, and scaletest.
- The `codersdk` Task SDK methods (`CreateTask`, `DeleteTask`, `Tasks`,
`TaskLogs`, etc.) and the Tasks feature (CLI, UI, API) are untouched;
this PR only removes MCP tool registration.
- The task-only `taskIDDescription` helper is deleted; `userDescription`
stays (used by `coder_create_workspace`).
The code change is deletion-only: 970 removed lines across `toolsdk.go`
and its test file, no added behavior.
## Validation
- `go build ./...`, `go test ./coderd/mcp/`, and targeted
`golangci-lint` pass. `go test ./codersdk/toolsdk/` passes all tests;
the package-level goleak failure (agentssh goroutines) reproduces
identically on unmodified `main` and is unrelated to this diff.
- `pnpm run lint-docs` passes on the docs change.
- Remote dogfood UAT ran against the toolsdk change and passed:
`tools/list` on both the hosted MCP endpoint and `coder exp mcp server`
confirms the six tools are absent and the remaining surface is intact
(34 hosted tools; CLI adds `search`/`fetch`), a `tools/call` of
`coder_get_authenticated_user` succeeds, and calling a removed tool
returns `-32602 unknown tool`. `coder_report_task` still registers in
the CLI server when `CODER_MCP_APP_STATUS_SLUG` is set.
- The failing `Pixel / Review` commit status is unrelated: this PR
contains no `site/` changes, and the same status fails on the last 8
`main` commits including this PR's merge base (`676b9bb06bf`).
> Mux acted on Mike's behalf to create this pull request.