Repeat of #27604, which I had mistakenly manually merged into #27603,
instead of letting Graphite handle it after #27603 merges into main.
Reattempting with GitHub stacks now instead of Graphite!
See #27604 for details/review.
## Summary
In the workspace delete confirmation dialog, the text typed into the
"Workspace name" confirmation field rendered in the same secondary color
as the placeholder, making it hard to distinguish entered text from the
placeholder.
The shared `Input` component uses `text-inherit`, so it picked up the
dialog description's secondary text color. This sets the confirmation
input's text to `text-content-primary` (white on dark), while the
placeholder remains `text-content-secondary`.
## Changes
- `WorkspaceDeleteDialog.tsx`: add `className="text-content-primary"` to
the confirmation `Input`.
##### Before
https://github.com/user-attachments/assets/88dbb230-feb6-416c-b516-9cc92950a0b8
##### After
https://github.com/user-attachments/assets/74296b9e-cbdf-40fe-ba3d-6fbcf5d4a9d5
---
_This PR was generated by Coder Agents on behalf of @jeremyruppel._
The "AI budget" column shows spend against the configured limit, not the
limit itself, so "AI spend" describes what it actually displays. Renamed
in both the groups table and the group members table.
Also replaces "estimated" with "approximate" in the surrounding spend
copy, which reads more plainly as a signal that the amount isn't exact.
The user dropdown now keeps the date range on a single line, so the
label wraps above it instead of the dates splitting across two lines.
Before:
<img width="250" height="89" alt="Screenshot 2026-08-10 at 12 55 55"
src="https://github.com/user-attachments/assets/428f7032-0ead-44ac-987d-563e20c97d12"
/>
After:
<img width="248" height="92" alt="Screenshot 2026-08-10 at 12 57 09"
src="https://github.com/user-attachments/assets/cf0704ba-d0ab-4132-b207-a15ab2ea4f37"
/>
Related to internal slack thread:
https://codercom.slack.com/archives/C096PFVBZKN/p1786051712464009
> [!NOTE]
> Initially generated by Claude Opus 5, modified and reviewed by
@ssncferreira
Closes#27933. Related: #27897 (single-agent GET).
Agent metadata is only readable via a per-agent watch stream, so reading
it across N workspaces costs N+1 requests. This adds a batch read to the
list endpoint:
```text
GET /api/v2/workspaces?q=param:"pool=demo" include_agent_metadata:task_status
```
- New `include_agent_metadata` search key, repeatable and key-scoped. It
expands the response, it does not filter workspaces.
- `GetWorkspaces` aggregates the requested keys as JSON behind a `CASE`:
without opt-in the response is unchanged and the subquery never runs.
Runs only for the returned page, inside the same authorized query.
- Agents in the response gain `metadata`
(`[]codersdk.WorkspaceAgentMetadata`, `omitempty`), mapped by the
`workspace_agent_id` each element carries. The collection script is
omitted; it can be long.
- `codersdk.WorkspaceFilter` gains `IncludeAgentMetadata []string`.
- No wildcard, no schema change, no migration.
---
Authored by Coder Agents on behalf of @Emyrk.
## Summary
Removes the `Beta` feature stage badge from the `/agents` page
navigation:
- **`ChatsPanel.tsx`**: removed the badge from the desktop left sidebar
header
- **`AgentPageHeader.tsx`**: removed the badge from the mobile top
header
Both instances rendered `<FeatureStageBadge contentType="beta" size="xs"
/>` next to the Coder product logo. The `FeatureStageBadge` imports were
removed as they are no longer used in these files.
## Sidebar summary
- Removed Beta badge from desktop sidebar (`ChatsPanel.tsx`)
- Removed Beta badge from mobile header (`AgentPageHeader.tsx`)
- Removed unused `FeatureStageBadge` imports from both files
- No behavioral or logic changes, badge-only removal
---
PR generated with Coder Agents
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
Resolves
[DEVEX-460](https://linear.app/codercom/issue/DEVEX-460/defer-api-key-generation-in-useapplink-to-on-click-instead-of-page).
## Problem
`useAppLink` minted a session key on mount via a `useQuery`:
```ts
const { data: apiKeyResponse } = useQuery({
...apiKey(),
enabled: isExternalApp(app) && needsSessionToken(app),
});
```
Whenever any page mounted `useAppLink` for an external app that embeds
`$SESSION_TOKEN` in its URL (JetBrains Gateway, Coder Desktop, etc.), it
fired `POST /api/v2/users/me/keys` on render, even if the user never
clicked the link. Each call minted a fresh session key and produced a
`created token` audit-log entry. Simply navigating the dashboard
generated a stream of `created token` entries with no real connection
activity.
This is the follow-up to #22318 (AIGOV-24), which only fixed the
built-in VS Code / VS Code Insiders buttons in `WorkspacesTable`.
## Change
Mirror the on-click minting pattern from #22318, applied to the shared
`useAppLink` hook:
- Replace the eager `useQuery(apiKey())` with a `useMutation(() =>
API.getApiKey())` that runs only when the user clicks a token-bearing
external app. `onClick` mints the key, builds the final URL, and
navigates via `location.href` (these are always custom-protocol,
non-HTTP external apps).
- The returned `href` no longer embeds a token; the hook owns opening
for token apps.
- Replace the `hasToken` field with `isLoading` (mint in-flight) and
update call sites so token apps are always clickable, with a loading
affordance while a key is being minted.
- `TaskApps` external-app menu items now render an anchor with the
hook's `onClick` (instead of a `RouterLink` that bypassed it), so the
deferred mint runs there too.
No API key is minted until the user actually opens an external app.
## Affected call sites
- `site/src/modules/apps/useAppLink.ts` (core change)
- `site/src/modules/resources/AppLink/AppLink.tsx`
- `site/src/pages/AgentsPage/components/WorkspacePill.tsx`
- `site/src/pages/TaskPage/TaskApps.tsx`
## Testing
Added Storybook interaction tests (`play` functions) in
`AppLink.stories.tsx` that assert the fix directly:
- `ExternalAppDefersSessionToken`: a token-bearing external app mints
**no** key on render, and mints exactly one on click.
- `ExternalAppWithoutSessionTokenNeverMints`: an external app without
`$SESSION_TOKEN` never mints a key, even on click.
Also verified:
- `tsc -p .` (frontend typecheck) passes
- `biome check` on changed files passes
- `vitest` for `AppLink` (incl. new stories), `apps`, `WorkspacePill`,
`TaskApps`, and `AppStatuses` stories/tests pass
- pre-commit hook (gen/fmt/lint/build) passes
## Manual verification checklist (recommended before merge)
- With an external app that uses `$SESSION_TOKEN` (e.g. JetBrains
Gateway), open DevTools → Network filtered on `users/me/keys`, reload
`/workspaces` and the workspace detail page, and confirm **no** `POST`
fires on render.
- Confirm a `POST` fires only when the app link is clicked, and the app
opens correctly.
> 🤖 This PR was modified by Coder Agents on behalf of Jake Howell.
- Redesign Deployment Appearance settings to the standard two-column
settings layout (`HorizontalForm` / `FormSection`) for branding, with
announcement banners as a secondary `SettingsHeader` + table section
(aligned with OAuth2 apps / AI Governance).
- Demui `IconField` and reuse it for Logo URL (URL input, preview, emoji
picker); thin `IconPickerField` to wrap the shared component.
- Clean up announcement banner create/edit (draft-only until save,
page-top preview via portal, shadcn `Switch` / menu icons, empty-state
CTA) and clarify Premium paywall copy to cover branding and announcement
banners.
| Old | New |
| --- | --- |
| <img width="2936" height="2666" alt="appearancesettings_old"
src="https://github.com/user-attachments/assets/db11950d-2440-412b-a74d-fcf57f2467e9"/>
| <img width="2936" height="2714" alt="appearancesettings_new"
src="https://github.com/user-attachments/assets/3f418fb1-7f94-4fd8-bf54-0d63014c6533"/>
|
> 🤖 This PR was modified by Coder Agents on behalf of Jake Howell.
Replace MUI `TextField` / Emotion theming in `IconField` with
`InputGroup`, `Popover`, and Tailwind.
`IconPickerField` already had the demui'd `InputGroup` + `Popover`
implementation, so that was ported into the shared `IconField`. Per
review feedback, `IconPickerField` is now removed and its call sites
(the MCP server, provider, and OAuth app forms) use `IconField`
directly.
## What
Clicking a selected module in the right-hand `SelectionSummary` sidebar
now jumps to the module settings step and scrolls that module's card
into view.
Addresses [DEVEX-587](https://linear.app/codercom/issue/DEVEX-587). This
is an isolated slice of #27077 (which bundles several unrelated
changes); only the "click a module to scroll to it" behavior is included
here.
## Changes
- `SelectionSummary`: gains an optional `onNavigateModule` prop. When
provided, each selected module row renders as an accessible `<button>`
(hover + focus-ring) labeled `Configure <name>`; otherwise rows stay
non-interactive.
- `ModuleSettingsStep`: each module card wrapper gets a stable
`id={module-config-<id>}` scroll anchor plus `scroll-mt-24` so the
sticky top nav does not cover the title.
- `TemplateBuilderPageView`: adds `navigateToModule`, which switches to
the module settings step and scrolls the target module into view once it
renders. If the module settings step is skipped (no configurable
variables), the click is a no-op.
- `SelectionSummary.stories`: adds a `NavigateModuleClick` interaction
story and updates `WithLongNameModule` to the new button semantics.
## Explicitly out of scope
The rest of #27077 is not included: gallery height/card clamp,
sensitive-var banner relocation, trash-icon removal wiring, the
scroll-past required-field highlight subsystem, and the broader
navigable-sidebar work (step labels, base-row navigation, back-stack
semantics).
## Testing
- `pnpm check` (biome) clean
- `pnpm lint:types` (tsc) clean
- `pnpm vitest run --project=unit src/pages/TemplateBuilder` — 46 pass
- `pnpm vitest run --project=storybook src/pages/TemplateBuilder` — 34
pass (incl. new `NavigateModuleClick`)
<details>
<summary>Implementation plan / decision log</summary>
### Goal
Open a new PR containing only the changes that satisfy DEVEX-587:
clicking a selected module in the right-hand `SelectionSummary` sidebar
should jump to the module settings step and scroll that module's card
into view.
### Base has moved since #27077
PR #27077 was cut against an older `main`. Today's `main` was
refactored:
- Steps are URL-driven; `steps.ts` already provides `StepId`, per-step
`group` (1/2/3), and `nearestVisible()`.
- `TemplateBuilderPageView` already has `navigateToStep(index: number)`
and a `useEffect` that resets window scroll on every `currentStep.id`
change.
- The sidebar no longer has a deselect ("X") button. The PR's
entanglement between "make row a nav button" and "move deselect to a
trash icon" therefore does not exist on current `main`, so module
navigation can be added without removing behavior and without pulling in
the trash-icon item.
So the isolated diff was written against current `main`, not reused
verbatim from the PR. It is smaller than the PR's own hunks and does not
include `maxReachedStep`, step-label navigation, or base-row navigation.
### Decisions
- Scope for this PR: module-row navigation only.
- Skipped-settings fallback: no-op. When no selected module exposes
configurable variables, the `module-settings` step is skipped and
clicking a module row does nothing (there is no card to scroll to).
### Scroll timing
`navigateToStep` triggers a window scroll reset via an existing effect
keyed on `currentStep.id`. To cooperate, `navigateToModule` stores the
target module id in a ref and a follow-up effect (declared after the
scroll-reset effect, so it runs second) calls `scrollIntoView` inside
`requestAnimationFrame` once `module-settings` has rendered. When
already on `module-settings`, it scrolls immediately.
### Follow-up (deferred): full navigable sidebar
Not part of this PR, documented for later. The remainder of #27077's
item #2, rebased onto current `main`:
- `onNavigateStep?: (stepId: StepId) => void` on `SelectionSummary`.
- Clickable step labels: `Base Template` -> `base-infra`, `Modules` ->
`module-select`, `Customizations` -> `customizations`.
- Clickable selected base-template row -> `base-parameters` (fall back
to `base-infra` when that step is skipped for the chosen base).
- Back-stack semantics via a `maxReachedStep` prop so steps at or below
the furthest-reached group stay `complete` and clickable even after
navigating backward, while strictly-higher groups render as inert
`upcoming`.
- `StepIndicator` and `BaseTemplateSelection` render as `<button>` when
a navigation handler is supplied, else stay inert.
- Stories: `NavigationClicks`, `BackwardNavigation`, and
`UpcomingStepsInert`.
</details>
---
Coder Agents generated, on behalf of @aqandrew.
Implements Phase 2 item 5 of the chats query architecture: by-workspace
cache reconciliation. Stacked on #27892; base branch
`feat/chat-search-invalidation`.
## Problem
`chatsByWorkspace` mappings (flat `Record<workspaceId, chatId>`, IDs
only, archived chats filtered server-side) go stale on
archive/unarchive, workspace binding changes, and watch events (FINDINGS
2.3). Because the client cannot re-derive archived state from the cached
map, the only correct repair for a stale mapping is synchronous removal
plus family invalidation.
## Fix
- New `removeChatFromChatsByWorkspace(queryClient, chatId)` in
`site/src/api/queries/chats.ts`: value-match removal across the
by-workspace family, reference-preserving when nothing is removed
(mirrors the `patchChatMessages` no-op pattern).
- `archiveChat.onSuccess` and the AgentsPageLayout archive-and-delete
`onSuccess` synchronously remove the mapping; the existing
`onSettled`/explicit invalidations then reconverge.
- Watch handler: `deleted` branch removes then invalidates
(remove-before-invalidate ordering); `created` root branch invalidates;
merge path invalidates behind a new `shouldInvalidateChatsByWorkspace`
predicate (`status_change`, `action_required` only; `created`/`deleted`
have their own branches, title/summary/diff/context events do not move
`updated_at` ordering); `onOpen` reconnect invalidates for convergence.
- `useChatToolInvalidations`: workspace-binding tool completion
(`create_workspace`) also invalidates by-workspace; this is the only
reconciliation path on the embed route.
## Confirmed exclusions
- No optimistic patch in `updateChatWorkspace.onMutate`; the awaited
`onSettled` invalidation already converges.
- No cancellation guard for the by-workspace family (Phase 2 item 9
territory).
- No `createChat` change (already invalidates); no ACL or pin changes.
## Known constraints
- Cascade archives remove only the event's own chat ID from the mapping;
per-family-member `deleted` events plus family invalidation repair the
rest.
- The REST `patchChat` workspace-rebind branch publishes no watch event
server-side, so cross-session manual rebinds converge only via the
acting session's `onSettled` or a WorkspacesPage remount; not fixable
client-side.
## Testing
- `chats.test.ts`: removal scoping and reference-preservation tests;
`it.each` wiring tests (archiveChat/unarchiveChat/updateChatWorkspace
onSettled, createChat onSuccess invalidate by-workspace);
synchronous-removal assertion for `archiveChat.onSuccess`; negative
assertions for `updateChatTitle` and `createChatMessage`.
- `AgentsPageLayout.test.ts`: exhaustive `ChatWatchEventKind` table for
`shouldInvalidateChatsByWorkspace`.
- `useChatToolInvalidations.test.tsx`: create_workspace regression test
extended with a seeded by-workspace bystander.
PR generated by Coder Agents.
Stacked on `feat/chat-cache-semantic-ops`.
## Bug
Chat search results (`chatSearch` queries) were never invalidated, so
the search dialog served stale results after archives, renames,
deletions, new chats, message edits, and watch-driven status changes.
## Fix
Reintroduces `invalidateChatSearches`, a prefix invalidation over the
module-private `chatSearchFamilyKey`, and wires it into:
- `chats.ts`: `archiveChat.onSettled`, `unarchiveChat.onSettled`,
`updateChatTitle.onSettled`, `editChatMessage.onSettled`,
`createChat.onSuccess`
- `useChatStore.ts`: `upsertCacheMessages` (unconditional; assistant
message bodies are indexed too) and `replaceCacheMessages`
- `AgentsPageLayout.tsx`: the `deleted` and root `created` watch
branches, the merge watch branch (gated by a new exported
`shouldInvalidateChatSearches` helper), the `has_unread` clearing
effect, the `onOpen` reconnect convergence, and
`archiveAndDeleteMutation.onSuccess`
The merge-branch gate only invalidates for search-affecting event kinds
(`title_change`, `status_change`, `diff_status_change`,
`action_required`). `summary_change`, `chat_summary_change`, and
`context_dirty` are excluded: stale `last_turn_summary` subtitles are
accepted until reconciliation lands.
## Backend constraint
Message bodies only enter full-text search via the dbpurge backfill
(`search_tsv` starts NULL and is populated every 10 minutes). Frontend
invalidation fixes removals, ordering, and rendered fields immediately,
but a chat that newly matches on message body will not appear until the
next backfill. This is a server-side eventual-consistency limit we
accept.
## Scope decisions (confirmed)
- No invalidation in `createChatMessage.onSuccess`: the send path
already routes through `useChatStore.upsertCacheMessages`; adding both
would double-invalidate every send.
- No invalidation for `pinChat`/`unpinChat`/`reorderPinnedChat`
(ordering-only, self-heals).
- ACL mutations out of scope.
- No coalescing/debouncing; that belongs to a later reconciler PR.
## Tests
- Prefix invalidation: multiple distinct `q` params invalidated,
bystanders (list, by-workspace, entity, messages, cost tree) untouched.
- Mutation wiring: settlement of `archiveChat`, `unarchiveChat`,
`updateChatTitle`, `editChatMessage`, and `createChat` invalidates a
seeded search key; `createChatMessage` asserted NOT to.
- `shouldInvalidateChatSearches` unit-tested over all
`ChatWatchEventKind` values.
PR generated by Coder Agents.
> [!NOTE]
> These were previously returning as typed values in the `api.ts`,
however, they were not actually typed in this way and updating them
wouldn't have worked for the `promotion` as it would fall back to
needing to validate against the template.
Archive toasts showed `"undefined"` because the API does not return a
`TemplateVersion`. Toast/dialog copy now comes from the selected
version.
- Hold the full `TemplateVersion` for promote/archive confirms (global
pattern elsewhere)
- Name the version in confirm dialogs and success/error toasts
- Type archive/unarchive API helpers as `Promise<void>`
<img width="772" height="152" alt="image"
src="https://github.com/user-attachments/assets/0063ebdc-09a3-4041-bfa0-d4c70b57159b"
/>
Relates to CODAGT-713
Depends on #27514
Removes the legacy deployment-wide allowlist now that the API and frontend use per-template `agents_allowed`: the experimental `/template-allowlist` routes, SDK methods and generated types, site config queries, frontend bindings, and the now-unused `xjson` utility.
Migration `000563` deletes the obsolete `agents_template_allowlist` value. It's irreversible for deployments that configured an allowlist, which I think is fine, since `000562` already drops `agents_allowed` on the way down, and this release ships `000548` and `000555` with the same property.
Two side effects of the model change worth writing down, both from #27514 rather than here. The value used to need `ActionRead` on `ResourceDeploymentConfig` to read and deployment config update to write. `AgentsAllowed` is now a plain field on the template response, readable by anyone who can read the template, and it's set with a template update, so org admins manage it themselves. That's the delegation we wanted, and it's tracked in the audit log.
The rest of the stack adds `--agents-allowed` to the CLI and updates the platform controls docs.
Relates to CODAGT-713
Depends on #27285
This replaces the deployment-wide template allowlist UI with per-template Coder Agents controls. Each template's General Settings page exposes `agents_allowed` to organisation and template administrators through the existing template-update permission, whilst AI Settings > Templates gives deployment administrators a bulk management page. The bulk page requires both deployment-config and template-update permissions, reuses the same URL-backed server-side filter as `/templates`, tracks pending updates independently for each template, and surfaces failures through template-specific toasts. The copy refers to new workspace creation because disabling a template does not revoke an existing Coder Agent workspace.
This completes the frontend migration away from the experimental `/template-allowlist` API. #27515 removes the now-unused legacy storage, routes, SDK methods, generated types, and JSON utility. Later PRs add CLI flags and update the platform controls documentation. The stack is intended to merge as a unit.
Relates to CODAGT-713
Depends on #27284
This makes the per-template `agents_allowed` field authoritative in the API and chatd. It adds optional create and metadata update fields with the intended default and omission semantics, supports `agents-allowed:` template search, includes the value in telemetry, and makes `list_templates`, `read_template`, and `create_workspace` read the template row directly. Existing-workspace retries remain idempotent, and blocked same-organisation templates return an actionable message.
The experimental `/template-allowlist` routes remain temporarily because the shipped AI Settings page still calls them, but they no longer control chatd enforcement. #27514 moves that page to per-template metadata, #27515 removes the legacy storage, routes, SDK types, and utility, #27517 adds the CLI flags, and #27518 updates the platform controls documentation for the per-template model, directly addressing CRF-5 and CRF-6. The stack is intended to merge as a unit.
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
## Summary
Deletes the shared `Pill` component (`site/src/components/Pill/`) and
migrates every consumer to `Badge`. Accepts the intentional visual shift
(`rounded-full` → `rounded-md`, slightly different color tokens). No
`ThemeRole` variants were added to `Badge`.
### Key changes
- Added `site/src/components/Badge/themeRoleToBadgeVariant.ts`, a small
mapper used at call sites that receive a `ThemeRole` (e.g.
`StatusBadge`, `TemplateVersionStatusBadge`).
- Exported `BadgeProps` from `Badge.tsx` so the mapper and status
helpers can reference the variant type.
- Replaced `PillSpinner` with `<Spinner loading />`.
- Renamed `StatusPill` → `StatusBadge` (folder, file, and export) and
updated the audit log and connection log imports.
- Extended `Badge.stories.tsx` with a status + spinner icon pattern
(rather than porting the full Pill story).
### Variant mapping applied
| Pill `type` | Badge `variant` |
|---|---|
| `success` | `green` |
| `error` | `destructive` |
| `warning`, `danger` | `warning` |
| `active`, `notice` | `info` |
| `inactive`, `muted`, `info` | `default` |
| `preview` | `purple` |
### Out of scope
The local Health-page `Pill` in `site/src/pages/HealthPage/Content.tsx`
is a separate component with no shared import; it will be migrated in a
follow-up.
## Validation
- `pnpm check` (biome) passes
- `pnpm lint` (biome + tsc + circular-deps + compiler + knip) passes
- `pnpm test:storybook` for Badge, TemplatePageHeader, and AuditLogRow
stories: 30/30 pass
- Grep for `components/Pill` and `PillSpinner` in `site/src` returns
nothing
<details>
<summary>Implementation plan</summary>
# Migrate Pill to Badge
## Goal
Delete `site/src/components/Pill/Pill.tsx` and migrate every shared-Pill
consumer to `site/src/components/Badge/Badge.tsx`. Accept the visual
shift (`rounded-full` → `rounded-md`, slightly different color tokens).
Do not add ThemeRole variants to Badge.
**Out of scope:** the local Health-page `Pill` in
`site/src/pages/HealthPage/Content.tsx` (separate component, no shared
import). Rename/migrate that in a follow-up.
## Variant mapping
Use this fixed mapping at every call site (Pill `type` → Badge
`variant`):
| Pill `type` | Badge `variant` | Notes |
|---|---|---|
| `success` | `green` | |
| `error` | `destructive` | |
| `warning`, `danger` | `warning` | Pill treats danger like orange
warning |
| `active`, `notice` | `info` | sky / pending |
| `inactive`, `muted`, `info` | `default` | Pill `info` is neutral grey;
Badge `info` is sky, so map Pill `info` to `default` |
| `preview` | `purple` | |
Other remaps:
- `icon={node}` → put `node` as the first child of `Badge`
- `size="lg"` → `size="md"` (closest); add a local `className` only if a
specific layout breaks
- `PillSpinner` → `<Spinner loading />` from
`site/src/components/Spinner/Spinner.tsx`
- Prefer existing wrappers when they already exist (e.g.
`DeprecatedBadge` on the template header)
For ThemeRole-driven helpers (`httpStatusColor`, status getters), add a
tiny shared mapper next to Badge
(`site/src/components/Badge/themeRoleToBadgeVariant.ts`). Use it in
`StatusPill` and any site that still receives `ThemeRole` rather than
hardcoding variants twice.
## Call-site work (ordered easy → hard)
1. **Trivial label chips** (import swap + variant map):
TemplatePageHeader (use `DeprecatedBadge`), VersionRow,
ChangeWorkspaceVersionDialog, PermissionPillsList, IdpPillList,
OrganizationPills
2. **Icon-as-child chips**: Provisioner, ProvisionerTag (local
`BooleanPill` becomes a Badge wrapper), TemplateExampleCard (keep
clickable styles via `className`)
3. **Status + spinner**: TemplateVersionStatusBadge (Badge + children
icons; `PillSpinner` → `Spinner`), utils/workspace.tsx (replace
`PillSpinner` imports only)
4. **Wrappers that own ThemeRole**: StatusPill (switch to Badge +
`themeRoleToBadgeVariant(httpStatusColor(...))`; rename to
`StatusBadge`), Notifications (Badge with severity `className`
overrides)
5. **Delete shared Pill**: remove `Pill.tsx`, `Pill.stories.tsx`, and
the `components/Pill` folder; extend `Badge.stories.tsx` only if a
migrated pattern is missing (icon + spinner status)
## Non-goals / constraints
- Do not change Badge's default shape or add `rounded-full`
- Do not broaden Badge's public API with `type` / `icon` props (that
recreates Pill)
- Do not rename IDP/permission "PillsList" filenames unless needed for
imports
- Modify Badge itself only for the new mapper helper (or stories)
## Validation
- `pnpm check` / `pnpm lint` under `site/`
- Storybook smoke on Badge + affected pages
- Grep for `components/Pill` and `PillSpinner` must be empty in
`site/src`
</details>
Migrates the New workspace template picker off MUI Link, Emotion `css`,
and the custom Popover/MenuSearch stack onto the shared Combobox used by
filters.
Selecting a template still navigates to create a workspace. The list
max-height uses the popover's available height so the footer stays
visible and scrolling works on short viewports.
Migrate the organization custom role create/edit form off MUI onto
shared site components.
- Replace `TextField` with `FormField` for name and display name
- Replace permission checkboxes with the shared shadcn `Checkbox`
(including indeterminate)
- Replace the “show advanced permissions” control with a `Switch` +
`Label`
- Update the toggle story to query by accessible checkbox role/name
Adds a hidden `--chat-hook-allow-insecure` /
`CODER_CHAT_HOOK_ALLOW_INSECURE` deployment option (default `false`)
that allows the chat lifecycle hook URL to use plain HTTP for any host.
The HTTPS requirement is enforced at two points, and the flag relaxes
both: `DeploymentValues.Validate()` rejects `http` hook URLs at startup,
and the hook dispatcher's `validateHookURL` allows `http` only for
loopback hosts. With the flag set, any-host `http` is accepted; the
host, fragment/userinfo, secret, and timeout checks are unchanged, and
non-http(s) schemes still fail. This removes the need for an HTTPS
reverse proxy when testing a hook consumer on a trusted network.
Following security review feedback, the flag description and docs state
that plain HTTP lets an on-path attacker forge hook responses (which
control agent execution), and `coder server` logs a startup warning
(with a redacted hook URL) when hooks run over plain HTTP.
Docs, generated API types, and the server config golden are updated
accordingly.
> Mux acted on Mike's behalf to create this PR.
fixes DEVEX-70
## Summary
Fixes#22350.
`getAppHref()` in `site/src/modules/apps/apps.ts` called `new
URL(app.url)` unguarded for external apps. When a template author
configures a `coder_app` with an unparseable `url` (e.g. a bare string
like `"my-repo"` with no scheme), `new URL()` threw `TypeError: Failed
to construct 'URL': Invalid URL` during render. Because `getAppHref`
runs inside `useAppLink` (used by both `AppLink` and the workspaces
table `IconAppLink`), the exception crashed the entire Workspace List
and Workspace detail pages, not just the affected app button.
## Changes
- `getAppHref()` no longer throws: the external-app protocol parse is
wrapped in `try/catch`, so an unparseable URL falls back to the raw
value instead of crashing.
- Added `isExternalAppUrlInvalid(app)`, a pure predicate used by
consumers to decide whether the app can be launched.
- `AppLink` renders a disabled button with a warning icon and an
explanatory tooltip when the URL is invalid, mirroring the existing
"admin has not configured subdomain application access" pattern. The
tooltip points the user at the responsible configuration:
> This app has an invalid URL and can't be opened. Ask your template
administrator to fix the app's `url` in the template's `coder_app`
configuration.
- `IconAppLink` (workspaces table) renders a non-navigating icon with an
equivalent label for invalid URLs.
## Testing
- Unit tests in `apps.test.ts`: `getAppHref` no longer throws for
invalid URLs, plus coverage of `isExternalAppUrlInvalid`.
- New `InvalidExternalAppUrl` Storybook story with a `play` function
asserting the button is disabled and the tooltip explains the invalid
URL.
- `pnpm exec vitest run` (unit + storybook), `pnpm exec tsc --noEmit`,
and `biome check` all pass.
<details>
<summary>Implementation plan</summary>
# Plan: Handle invalid `coder_app` URLs gracefully (issue #22350)
## Problem
`getAppHref()` in `site/src/modules/apps/apps.ts` calls `new
URL(app.url)`
unguarded for external apps. When a template author sets an external app
with
an unparseable `url` (e.g. a bare string like `"my-repo"` with no
scheme),
`new URL()` throws during render. Because `getAppHref` runs inside
`useAppLink` (called during render of `AppLink` and `IconAppLink`), the
exception propagates and crashes the entire Workspace List and Workspace
detail pages, not just the single app button.
## Goal
Never throw from `getAppHref`. Detect the invalid-URL case and let the
UI
render a disabled button with an explanatory tooltip, mirroring the
existing
`isAppBlockedByMissingWildcard` pattern.
## Approach
1. Make `getAppHref` non-throwing (defensive), so no render path can
crash.
2. Add a pure predicate `isExternalAppUrlInvalid(app)` used by button
components to decide whether to disable and what tooltip to show.
3. Wire the predicate into `AppLink` and `IconAppLink`.
## Changes
- `apps.ts`: wrap the external protocol parse in `try/catch`; add
`isExternalAppUrlInvalid`.
- `AppLink.tsx`: disabled state with `text-content-warning` icon and a
`ReactNode` (React Fragment) tooltip with `url` and `coder_app` wrapped
in
inline `<code>` elements.
- `WorkspacesTable.tsx` (`IconAppLink`): render a non-navigating icon
when
the URL is invalid.
## Tests
- `apps.test.ts`: `getAppHref` does not throw for invalid URLs;
predicate
coverage.
- `AppLink.stories.tsx`: `InvalidExternalAppUrl` story with a `play`
function
asserting disabled button and tooltip.
## Out of scope
Backend/template-side validation of `coder_app.url` would prevent the
misconfiguration at its source; tracked by the parent epic (#22349 /
DEVEX-60).
</details>
---
*Opened by Coder Agents on behalf of @aqandrew.*
This PR mainly replaces manually written table loader markup with our
`TableLoader` component, inspired by #27583.
## other changes
- adds some stories for some loading render paths previously untested in
Storybook:
- loading `OrganizationMembersPageView`
- loading `OrganizationProvisionerKeysPageView`
- `OrganizationMembersPageView`'s `NoMembers` story showed nothing (not
even an empty state) + erroneously displayed "Showing 1 to 2 of 2
members" below the table. Adds an empty state to
`OrganizationMembersTableBody` ("No members in this organization") +
updates the `membersQuery` arg in the `NoMembers` story to give an
accurate 0-member count
### before
<img width="1840" height="1191" alt="image"
src="https://github.com/user-attachments/assets/7dc310c8-286e-48a8-a18e-e4ae29afdd8b"
/>
### after
<img width="1840" height="1191" alt="image"
src="https://github.com/user-attachments/assets/206b2952-2758-4626-ba42-4c366d7902a6"
/>
## What
Adds CI enforcement that fails when docs Markdown contains invalid
inline HTML
the docs site silently drops or mangles, and fixes the remaining
generated-doc
placeholders at their source.
This is the tooling half of the docs-HTML audit. The hand-written fixes
it
guards landed in #27298 (kept small and separate so it reviewed fast);
this PR
carries everything that touches code, CI, or generated output.
## Changes
**Linter (`scripts/docshtmlcheck`), wired into `make lint` via
`lint/docs-html`.**
Markdown-aware: parses each file with goldmark and inspects only
raw-HTML nodes,
so angle brackets in fenced code blocks, inline code, HTML comments, and
`<https://…>` / `<user@host>` autolinks are ignored. Flags swallowed
placeholders (`<region>`), void-element end tags (`</br>`), unregistered
or
incorrectly capitalized component tags (`<Image>`), and unclosed
container tags (a
`<div class="tabs">` that leaks its wrapper). The one intentional
renderer
component, `<children>`, is allowed but still balance-checked.
**Generator-source placeholder fixes (regenerated via `make gen`).**
- `codersdk/chats.go`: backtick `<server>__` in the
`ChatContextTool.Name` doc
comment (it becomes the Swagger description, so it was swallowed in
`reference/api/{chats,schemas}.md`).
- `codersdk/deployment.go`: backtick `<region>` in the AWS Bedrock
region flag
help (swallowed in `reference/cli/server.md`); also updates `coder
server
--help` output and the golden files.
**Temporary allowlist.** `docs/reference/cli/agent-firewall.md`'s
`<host>` /
`<glob>` come from the external `github.com/coder/boundary` CLI help
(still
`v0.10.0` on `main`), so they are suppressed on that one file. The
suppression
is self-clearing: if an allowlisted tag stops appearing on a scanned
file, the
linter reports `stale-allowlist-entry` and fails until the dead entry is
removed, so a dead entry cannot silently mask a later regression of that
tag on
that page. (An entry whose file is deleted outright is never rescanned,
but a
missing file yields no findings, so nothing hides behind it either.)
## Review feedback addressed
This tool + generator work was reviewed by Coder Agents Review while it
was
bundled into #27298. Addressed here:
- **P1:** tokenize each raw-HTML node as a whole instead of per source
line, so
a tag whose attributes wrap across lines is no longer torn in half. This
fixes
both the missed multi-line unclosed `<div>` (a leaked wrapper that
passed with
exit 0) and the spurious `stray-end-tag` on valid multi-line tags. Each
token
maps back to its own source line.
- Normalize allowlist lookup/report paths to a canonical repo-relative
form, so
the escape hatch no longer silently misses under absolute / `./` paths.
- Route generated-page findings to the generator source.
- Add `<search>` to the allowed set; reword the unknown-element message
to note
that a real element can be added to `allowedElements`.
- Self-clearing allowlist guard (above); rename `optionalEndTag(s)` and
`kindUnclosed(Tag)`; adopt `slices`/`maps` idioms; move the lint banner
to the
Makefile recipe; stop aliasing the input slice in `filterAllowed`.
- New tests: multi-line tokenization (both classes), interleaved
nesting, a
pinned line number, `collectMarkdown`, and the stale-allowlist guard.
### Round 2 (Coder Agents Review on this PR)
A second `/coder-agents-review` pass on this PR raised 16 findings;
addressed in
`fix(docshtmlcheck): catch self-closing containers and capitalized
tags`:
- **P2:** self-closing container tags (`<div class="tabs"/>`) were
ignored by
the HTML5 parser and leaked their wrapper like the open spelling; the
balance
check now tracks self-closing tokens too (CRF-1).
- **P2:** a capitalized component tag whose lowercase name is a real
element
(`<Table>`, `<Section>`) slipped through on the `allowedElements`
lookup. The
tokenizer lowercases tag names, so the check now reads the raw token and
reports any capitalized name as a component reference (CRF-2).
- Narrowed the `:` / `@` autolink skip to a real URI scheme or a dotted
`local@domain`, so `<region:id>` and `<user@host>` stay checked (CRF-3).
- Stale-allowlist findings now report against the linter source with no
line,
and count separately from invalid-HTML issues in the footer (CRF-7,
CRF-11).
- Comment / README / Makefile wording synced to the honest
capitalized-tag
behavior; added the deleted-file allowlist caveat and a note that
`allowedElements` is hand-maintained against the renderer (CRF-14,
CRF-17,
CRF-9).
- Internal cleanups (`pop` -> `matchEndTag`, extracted
`unclosedFinding`) and
new tests: self-closing, capitalized open/close, colon/at placeholders,
a
non-first-token line assertion, `isGeneratedDoc`, and the stale message
(CRF-12, CRF-13, CRF-1/2/3/4/5/16).
Two findings resolved without a code change:
- **CRF-8** (also wire `lint/docs-html` into `lint-light`): declined.
`lint-light` is the Go-free fast path; `lint/docs-html` needs the Go
toolchain, so it stays in the full `make lint`, which CI runs. Adding it
would
pull Go into the light path for no coverage gain.
- **CRF-9** (`allowedElements` <-> renderer coupling): documented with a
maintenance note in the `allowedElements` comment and tracked in
DOCS-597 for
a cross-repo sync/check decision.
Deferred (note, no current trigger): raw-text element interiors
(`<script>` / `<style>`) are not scanned for nested tags. No docs page
relies
on this today; noted for follow-up.
## Merge order
#27298 (the hand-written fixes this PR guards) has merged, and this
branch is
rebased on `main`, so `make lint/docs-html` now reports 0 findings and
the
`lint` check passes. The two PRs are independent (disjoint files, no
stacking).
## Verification
- `go test ./scripts/docshtmlcheck/`, `go vet`, `gofmt -l`,
`golangci-lint run`: clean.
- `make lint/docs-html` (branch rebased on `main`): 0 findings.
## Linear
- DOCS-584:
https://linear.app/codercom/issue/DOCS-584/add-ci-check-that-fails-on-invalid-inline-html-in-docs
- DOCS-551:
https://linear.app/codercom/issue/DOCS-551/backtick-placeholder-syntax-in-generated-reference-docs-cli-help
- DOCS-597 (follow-up, from CRF-9):
https://linear.app/codercom/issue/DOCS-597/track-docshtmlcheck-allowedelements-drift-vs-docs-renderer-component
> This PR was created with AI assistance (Coder Agents).
## Summary
Standardize table empty and error rows on shared `TableEmpty` so they
share the same large empty treatment instead of one-off markup or manual
`EmptyState` wrappers.
- Replace hand-rolled `text-center` empties (External Auth, OAuth2
apps/secrets/authorized apps, Tasks)
- Swap `TableRow`/`TableCell` + `EmptyState` wrappers to `TableEmpty`
across audit/connection logs, groups, roles, IdP sync, provisioners,
users, versions, permissions, banners, and workspace sharing
- Drop redundant padding wrappers on Users and Versions tables
- Add an Empty story for External Auth settings
Fixes several Storybook play function failures that show up in Pixel
(and some that fail under `vitest --project=storybook`).
### Component / behavior
- **DurationField**: restore digit-only filtering so non-numeric input
is stripped again. A demui pass had stopped filtering and relied on
`pattern`, which does not prevent typing.
### Pixel viewport mismatches
Pixel ignores Storybook viewport params. Shared helpers
`pixelWithDesktop` / `pixelWithPhone` live in `testHelpers/pixel.ts`.
- **NavbarView**: admin dropdown stories are desktop-only. Pixel tablet
is 744px, below Tailwind `md` (768px), so Admin settings is hidden and
the play click fails. `MobileMenu` already covers the collapsed navbar.
- **AgentPageHeader**: mobile meatball menu story is phone-only. At
laptop width `sm:hidden` keeps "More options" out of the a11y tree even
when `matchMedia` is mocked as mobile.
### Story assertion fixes
- **WorkspaceSettingsPageView**: Formik passes helpers as a second
`onSubmit` argument; the assertion now allows that.
- **AgentChatPageView**: sidebar tab persistence stories use Git instead
of Terminal. Clicking Terminal mounts xterm, which throws an unhandled
`dimensions` error and fails the run even when tab assertions pass.
- **NetworkCallsTable / Blocked Badge**: badge copy is split across an
`sr-only` span and the count; assertion matches combined `textContent`.
- **CreateOAuth2AppPageView / Default**: wait for Formik
validate-on-mount before asserting the submit button is disabled.
- **NotificationEvents / Change Method** (+ Error): combobox accessible
name is now `Notification method for ${template}`; match with a regex.
- **DynamicClientRegistrationSetting / Keeps Focus While Updating**:
wait for the Enable label after the harness finishes the request.
Pixel logs play failures but still exits 0, so these can stay green in
CI while failing in the Pixel report.
---------
Co-authored-by: Danielle Maywood <danielle@themaywoods.com>
> 🤖 This PR was modified by Coder Agents on behalf of Jake Howell.
Continue the MUI → Tailwind/shadcn migration across a few auth and
settings surfaces.
- Create token form: MUI `TextField`/`MenuItem` → `FormField`, `Select`,
and `Input` (drops Emotion for section min-width)
- Create organization form: `TextField` → `FormField` / `Textarea`,
matching the organization info form
- SSO security section and external auth: MUI `Link`/`TextField` →
shared `Link`, `Input`, and `Label`
- Permission and IdP pill lists: MUI `Stack` → `flex flex-row gap-2`
- Minor link layout polish on Git device auth / external auth pages
---
_Also removes the now-unused `@emotion/css` dependency from
`site/package.json` (and the lockfile), which was the last usage of it.
This fixes the `knip` CI lint failure ("Unused dependencies:
@emotion/css")._
fixes DEVEX-486
From `TemplateVersionsPage`
(/templates/:organization/:template/versions), users navigate to
individual template version pages (`TemplateVersionPage`, i.e.,
/templates/:organization/:template/versions/:version) by clicking
`VersionRow`.
This PR makes this click-to-navigate behavior on `VersionRow` available
to admin/owner users only, per @matifali's suggestion
## Summary
Removes the "Introductory access to Coder Agents through September 2026"
text and its now-unused `docs` import from the `/agents` page
(`AgentCreateForm.tsx`).
## Changes
- Removed the `<p>` element containing the introductory access link and
text below the chat input
- Removed the now-unused `docs` import (it was only referenced by that
link)
## Testing
No stories or tests referenced the removed text, so no test updates are
needed.
---
PR generated with Coder Agents
Updates the base infrastructure step, base parameters step, and module
screens in the template builder for typography consistency.
**Base infra step**
- Grid gains a `xl:grid-cols-4` tier while keeping `lg:grid-cols-3`, so
no width band gets looser than before.
- Card title: `text-sm font-bold`.
- Card description and `View details` link: `text-xs font-normal`.
**Base parameters step**
- Body copy in the README markdown unified to `text-xs`.
- List item text and `::marker` inherit `text-content-secondary` so
bullets match paragraph color.
- Inline `<code>` inherits body size instead of hardcoded `text-sm`.
- `ConfigurationField` description slots (`SelectField`, `RadioField`,
`SwitchField`, `SwitchGroupField`) now use `text-xs`. The `TextField`
path already rendered `text-xs` via `FormField` and is unchanged.
- Headings unchanged.
**Module screens (for consistency)**
- `ModuleSelectStep`: same additive `xl:grid-cols-4` tier; empty-state
copy moves to `text-xs`.
- `ModuleCard`: matches `TemplateCard` (title `text-sm font-bold`,
description and link `text-xs font-normal`).
- `ModuleConfiguration`: 'No configuration required' notice moves to
`text-xs`.
- `ModuleSettingsStep`: sensitive-variable notice moves to `text-xs`.
- `TemplateCustomizationsStep` `BaseTemplateCard` heading matches card
title style (`text-sm font-bold`).
---
_This PR was generated on behalf of @tracyjohnsonux by the Coder agent._
Migrate agent row log expand/collapse and agent status troubleshooting
link off MUI onto shared Collapsible and Link primitives.
Replaces `Collapse` in `AgentRow` with `Collapsible` /
`CollapsibleTrigger` / `CollapsibleContent`, and swaps the MUI `Link` in
`AgentStatus` for the shared `Link` component.
Migrate the create template gallery page and create template form off
MUI and Emotion onto Tailwind and shared form/link primitives
(`FormField`, `Textarea`, `Link`).
Replaces the MUI `Snackbar` / `Link` update-check toast in
`DashboardLayout` with a Tailwind `UpdateCheckNotice`, and adds
Storybook coverage for the layout and notice.
The notice stays declarative (not Sonner), offsets above the deployment
banner when the user can view deployment stats, and otherwise sits at
the normal bottom margin.
| Old | New |
| --- | --- |
| <img width="460" height="158" alt="UPDATE_NOTICE_OLD"
src="https://github.com/user-attachments/assets/d7ac9e5a-f69a-47de-89d5-9b6c6df4e1aa"
/> | <img width="440" height="157" alt="UPDATE_NOTICE_NEW"
src="https://github.com/user-attachments/assets/a5eee05b-0232-4668-bc3e-fd3ffdbad9b8"
/> |
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.
Removes the remaining MUI/Emotion usage from the Template Settings
pages, moving them over to Tailwind and the shared component primitives.
## What
- **`<TemplateScheduleForm />`** — deMUI'd, along with the shared
**`<DurationField />`** it relies on.
- **`<TemplateVariablesForm />` / `<TemplateVariableField />`** —
deMUI'd.
- **`<TemplateSettingsForm />`** (general settings) — deMUI'd.
No behavioural changes intended; this is a styling/plumbing refactor.
## Stack Context
This stack makes AI Gateway data and budgets the source of truth for AI
spend controls.
1. Re-back the per-chat cost endpoint with AI Gateway data (#27328,
merged).
2. Remove native chat usage limits (#27329, merged).
3. **This PR, now based on `main`:** remove native chat cost tracking
and its dedicated admin UI.
## Summary
Removes native per-message price calculation, model pricing fields, cost
persistence, aggregate cost queries, and admin cost API types. It also
deletes the Analytics and Spend pages plus their legacy redirects. The
AI Gateway-backed per-chat cost row and compact budget indicators
remain.
The spend documentation is renamed to `spend-management.md` and updated
for the remaining surfaces, group budget APIs, CSV export, upgrade
handling for native pricing and cost history, and the absence of a
deployment-wide spend dashboard. The per-chat cost API documents that
data follows AI Gateway retention and reports zero after all matching
requests are purged.
No schema is dropped in this release. `chat_messages.total_cost_micros`
remains nullable and unwritten so replicas from the previous release can
continue inserting messages during rolling upgrades. #27600 tracks
removal after the compatibility window.
> Mux prepared this PR on Mike's behalf.
## Stack Context
This stack makes AI Gateway data and budgets the source of truth for AI
spend controls.
1. Re-back the per-chat cost endpoint with AI Gateway data (#27328,
merged).
2. **This PR:** remove native chat usage limits.
3. Remove native chat cost tracking and its dedicated admin UI (#27330).
## Summary
Removes the native usage-limit API, SDK types, SQL, and chat enforcement
for deployment, user, and group chat limits. Compact AI Gateway budget
indicators remain in the Agents sidebar, user menu, and group settings.
Gateway budget rejections and provider quota failures continue to
classify as usage-limit errors, including a 409 response for synchronous
title generation.
Budget-period labels now use the API's UTC boundaries, so users see the
same dates in every browser timezone. The documentation explains the AI
Gateway replacement, its licensing requirements, and the differences
from native limits.
No schema is dropped in this release. The usage-limit table, index, user
and group columns, constraints, audit mappings, and generated scan
fields remain for mixed-version rolling upgrades. #27600 tracks their
removal after the compatibility window.
## Breaking change
Native day, week, and month chat spend limits are removed and are not
migrated. AI Gateway budgets are month-based, group-scoped with per-user
overrides, and require the AI Gateway entitlement. Deployments without
that entitlement no longer have chat spend enforcement.
> Mux prepared this PR on Mike's behalf.
chatd hardcoded `WithUseResponsesAPI()`, so the provider SDK's static
known-model list decided whether an OpenAI model spoke the Responses API
or Chat Completions. A model absent from that list silently fell back to
Chat Completions until the fantasy fork was patched.
This exposes the SDK's `WithResponsesAPIFunc` hook as a per-model
setting, `openai_config.use_responses_api`, stored in the existing
`chat_model_configs.options` JSONB. Unset keeps the known-model list,
`true` forces Responses, `false` forces Chat Completions. There is no
migration.
It sits in a new construction-time `openai_config` section rather than
in `provider_options.openai` because it selects the API when the client
is built, while `provider_options` holds per-request parameters. That
placement is also load-bearing: a config setting only this field would
otherwise materialize an OpenAI request-options struct and turn on
provider-side response storage, since `Store` defaults to true there.
Three places independently decided the transport and would silently
disagree with the client actually built:
| Site | Effect when it disagrees |
| --- | --- |
| `ModelFromConfig` | the transport being overridden |
| `AcceptsFilePartMediaType` | text attachments dropped, since Responses
natively accepts only images and PDFs |
| `UsesResponsesOptions` | the SDK type-asserts the concrete options
struct, so every OpenAI option is discarded |
They share one predicate here, `chatopenai.UsesResponsesAPI`, with the
override threaded to each. The rest of the stack removes that threading
by resolving the transport once and carrying it. Compaction overrides
and the quickgen debug model built clients without `ConfigOptions`, so
they now pass it and pick up both this setting and the existing
Anthropic beta headers.
The toggle also makes transport-conditional option handling
admin-switchable, so two hardening changes ride along.
`ServiceTierFromChat` now maps every tier the codersdk enum advertises
(`auto`, `default`, `flex`, `scale`, `priority`); it previously returned
nil for `default` and `scale`, so flipping a model to Responses silently
dropped a configured `service_tier` that the API accepts (fantasy
forwards the value unchanged). And a new
`TestProviderOptionsTransportParity` pins, per `provider_options.openai`
field, which transport honors it, against a table in ARCHITECTURE.md, so
a field honored on one transport and silently ignored on the other fails
the test unless recorded as intentional.
Review rounds also caught two lifecycle gaps around the new field.
`isZeroChatModelCallConfig` now inspects `OpenAIConfig`, so a stored
options blob whose only setting is this toggle survives into GET/list
responses instead of reading as `model_config: null`;
`TestIsZeroChatModelCallConfigCoversEveryField` sets each config field
in isolation and fails if any field is invisible to the zero check. And
the model editor's update path sends an explicit empty `model_config`
when an edit clears the last field, since an omitted property preserves
the stored options server-side; covered by the
`EditClearingLastOptionSendsEmptyConfig` story.
Azure keeps following the known-model list, because the Azure provider
exposes no equivalent hook. The model editor renders Azure with the
OpenAI option schema, so instead of shipping a visible but inert
control, the option schema generator gains a `providers` struct tag that
it emits as `visible_for_providers`. Gating uses the raw provider type
rather than the alias table, so the control appears only for
openai-typed providers. No hand-written frontend field: the editor
renders it from the generated schema.
Closes
https://linear.app/codercom/issue/CODAGT-874/add-completionsresponses-api-toggle-in-model-editor
> Mux prepared this PR on Mike's behalf.
Renders the per-call network calls as a collapsible panel above the
session threads, using the `network_call_logs` field added to the
threads API.
Each row shows the request method, allowed/blocked status, URL, and
timestamp, and expands to show the protocol, matched rule, and full
detail. The header shows the total count and a blocked-count badge from
the session summary; when the server caps the list, the panel notes how
many calls are shown. The panel is omitted when the session did not pass
through Agent Firewall.
### PR map (merge strictly bottom-up)
This change is a 4-PR stack. Each PR depends on all the ones below it,
so merge in this exact order:
1. #27417 — backend network summary (base `main`)
2. #27418 — frontend summary rows (base #27417)
3. #27425 — backend per-call list `network_call_logs` (base #27418)
4. #27426 — frontend network-calls panel (base #27425)
Refs AIGOV-464
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>