Commit Graph
4744 Commits
Author SHA1 Message Date
Cian Johnston 444fb8aa9b fix(site): allow single-label AI provider endpoints (#28122)
Closes #27980.

Reduces the `baseUrl` field validation in `ProviderForm.tsx` to only
validate non-empty input. The previous validation was not in line with
`validateAIProviderBaseURL` in `codersdk/aiproviders.go`. This was
blocking users from adding providers with a short-form hostname (e.g.
`http://localhost:8080/v1`).

> Generated by Coder Agents, reviewed by a human.
2026-08-18 00:18:26 +01:00
Asher b5d18bb9c9 feat: add redirect URL override for external auth (#28082) 2026-08-17 14:09:23 -08:00
Jeremy RuppelandSamuel Volin 46ec620767 fix(site): deflake adjust user theme preference (#28219)
## Summary

Deflakes the `adjust user theme preference` Playwright test
(`site/e2e/tests/users/userSettings.spec.ts`), tracked in DEVEX-415.

The test selected the Light theme and then hard-navigated with
`page.goto`
before the optimistic appearance update was persisted. The navigation
could
cancel the in-flight `PUT /api/v2/users/me/appearance`, so the reloaded
document embedded the stale `dark` preference and the final assertion
flaked.
`toPass` retries could not help because retrying the reload only
re-reads the
still-stale persisted state.

## Fix

Wait for the appearance form's save spinner to clear before the hard
reload,
mirroring how other settings tests wait for a visible save confirmation.
Asserting the optimistic light class first guarantees the spinner is
already
showing if a save started; a repeat run that is already light never
shows it,
so the test is idempotent and there are no direct API calls.

To give the test a UI signal, `Spinner` gets an opt-in `label` prop that
exposes it as a `role="status"` live region with an `aria-label`
(decorative
otherwise). `Loader` moves its label onto its own status container so it
keeps
a single status region.

## Changes

- `site/src/components/Spinner/Spinner.tsx`: opt-in `label` prop.
- `site/src/components/Loader/Loader.tsx`: label on its own status
region.
- `site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx`:
label
  both appearance save spinners.
- `site/e2e/tests/users/userSettings.spec.ts`: wait for the save
spinner.

## Validation

- `biome check` and `tsc -p .` pass.
- Loader + AppearancePage unit tests and the affected storybook tests
pass.
- The e2e test passed 20/20 under
`pnpm playwright:test -g "adjust user theme preference" --repeat-each
20`.

<details>
<summary>Implementation plan & decision log</summary>

# DEVEX-415: Fix flake in "adjust user theme preference" e2e test

## Problem

Playwright test `site/e2e/tests/users/userSettings.spec.ts` → `adjust
user
theme preference` flakes. After selecting the Light theme and
hard-navigating
to `/`, the reloaded page sometimes stays `dark`, failing the final
assertion.

CI Flake Bot has recorded repeated recurrences on `main` (latest
2026-08-17,
runs `32004239187`, `31745261984`) even after PR #25183 added `toPass`
retries.

## Root cause (confirmed by reading the code)

The appearance update is optimistic and its persistence is not awaited
before
navigation:

- `updateAppearanceSettings` (`site/src/api/queries/users.ts`) has an
`onMutate` that optimistically writes the new theme into the React Query
  cache. The `<html>` class flips to `light` immediately, before the
  `PUT /api/v2/users/me/appearance` completes.
- `useQueuedAppearanceSubmit`
  (`site/src/pages/UserSettingsPage/AppearancePage/AppearancePage.tsx`)
serializes submits: if a request is in flight, the next is queued and
only
  fires after the first settles.
- A fresh `member` user has **empty** appearance settings.
`migrateLegacyPreference` (`site/src/theme/themeMode.ts`) maps empty
settings
to `{ mode: "single", theme: "dark" }` (`DEFAULT_THEME = "dark"`). So
the
"Theme mode" dropdown already starts on **Single theme** and `<html>`
starts
  `dark`. Selecting "Single theme" in the test is therefore a **no-op**
(`onChangeMode` early-returns when `mode === draft.mode`) and fires
**no**
  PUT. The only appearance PUT in the test is the one from clicking
"Light default" (`onSelectSingle("light")` → `theme_preference:
"light"`).
- The test's first `expectLightThemeClasses(page)` passes purely from
the
  optimistic cache. It then calls `page.goto("/")` almost immediately
  (~20 ms after the click). If PUT #2 (the light one) has not persisted
server-side, the new document loads the still-persisted `dark`
preference
from embedded metadata, and every retry of the post-navigation assertion
  sees `dark` for the full 10 s window.

`toPass` cannot help post-navigation because it only re-reads stale,
already
persisted state; it cannot make an unfinished/queued PUT complete.

## Implemented fix: wait for the saving spinner (UI signal) before
navigation

> Iteration history: (1) An earlier attempt waited on the appearance
`PUT` via
> a `waitForApiCall` helper, but that couples the test to a state
transition
> and is non-idempotent (a repeat run that is already light fires no
PUT, so
> the wait times out). CI retries reuse the same ephemeral server, so
this is a
> real hazard. (2) A second attempt confirmed persistence with
> `page.request.get(...)`, but that calls the API directly and stops
being a
> site test. Both were rejected.

The repo's non-flaky settings tests wait for a **visible save
confirmation**
(e.g. "settings updated successfully" toasts) before trusting the
result. The
appearance theme form has no toast; its only save-in-progress feedback
is the
`<Spinner>`. So the fix mirrors that pattern using the spinner:

1. Make the theme section's spinner identifiable via a new opt-in
`label` prop
   on `Spinner` (sets `role="status"` + `aria-label` only when provided;
   decorative otherwise). `Loader` moves its label onto its own `status`
   container so it keeps a single status region and its `getByLabelText`
   queries keep working.
2. In the test, after clicking "Light default", assert the optimistic
light
class, then wait for that spinner to be hidden before `page.goto("/")`.

Why this is correct and idempotent:

- React Query sets `isPending` before `onMutate` applies the optimistic
cache
update, so by the time the optimistic light class is visible the spinner
is
already showing if a save started. Waiting for it to clear guarantees
the PUT
  settled (and was not canceled by navigation) before the reload.
- A repeat run that is already light: clicking "Light default" is a
no-op radio
  change, no PUT fires, the spinner never shows, and `toBeHidden` passes
  immediately. The reload still shows light.
- No direct API calls: the test only observes site UI.

### Implemented changes

`Spinner` gains an opt-in `label` prop
(`site/src/components/Spinner/Spinner.tsx`):

```tsx
role={label ? "status" : undefined}
aria-label={label}
```

`Loader` carries the label on its own status container
(`site/src/components/Loader/Loader.tsx`), and the appearance save
spinners use
the new prop (`AppearanceForm.tsx`):

```tsx
<Spinner loading={isUpdating} size="sm" label="Saving theme preference" />
<Spinner loading={isUpdating} size="sm" label="Saving terminal font" />
```

`site/e2e/tests/users/userSettings.spec.ts`:

```ts
await expect(
	page.getByRole("combobox", { name: /theme mode/i }),
).toContainText("Single theme"); // precondition: single mode

const singleThemeGroup = page.getByRole("group", { name: "Theme" });
await expect(singleThemeGroup).toBeVisible();
await singleThemeGroup.getByText("Light default", { exact: true }).click();

await expectLightThemeClasses(page); // optimistic DOM => spinner showing if saving
await expect(
	page.getByRole("status", { name: "Saving theme preference" }),
).toBeHidden(); // save settled before the hard reload

await page.goto("/", { waitUntil: "domcontentloaded" });
await expectLightThemeClasses(page);
```

Validation: `biome check`, `tsc -p .` pass; Loader + AppearancePage unit
tests
and the affected storybook tests pass; the e2e test passed 20/20 under
`pnpm playwright:test -g "adjust user theme preference" --repeat-each
20`
(idempotent).

## Alternatives considered

- **Wait on the appearance `PUT` via a `waitForApiCall` helper**:
rejected. It
couples the test to a state transition and is non-idempotent, a repeat
run
that is already light fires no PUT so the wait times out (14/15 repeats
  failed). CI retries reuse the same ephemeral server, so this is a real
  hazard, not just a local-repeat artifact.
- **Confirm persistence with `page.request.get(...)`**: rejected. It
calls the
  API directly and stops being a site test.
- **Default `role="status"` on the shared `Spinner`**: rejected.
`Loader` wraps
  `Spinner` in its own `status` div, so a default would nest two status
regions and break `Loader.test.tsx`. The opt-in `label` prop avoids
this.
- **Add a networkidle wait or sleep before navigation**: rejected.
Violates
the repo guidance against `time.Sleep`-style timing hacks and is
inherently
  racy.
- **Fix product behavior instead of the test**: out of scope. Related
bug
DEVEX-94 ("Light Theme setting not respected until Appearance page
opened")
tracks product-side persistence/embedding behavior; this task is scoped
to
  stabilizing the e2e test.

## Files touched

- `site/src/components/Spinner/Spinner.tsx` (new opt-in `label` prop).
- `site/src/components/Loader/Loader.tsx` (label on its own status
region).
- `site/src/pages/UserSettingsPage/AppearancePage/AppearanceForm.tsx`
(use the
  `label` prop on both appearance save spinners).
- `site/e2e/tests/users/userSettings.spec.ts` (wait for the spinner).


</details>

---
This PR was created by Coder Agents on behalf of @jeremyruppel.

---------

Co-authored-by: Samuel Volin <sam.volin@coder.com>
2026-08-17 15:36:46 -04:00
Andrew Aquino 039c0da5ae feat(site/src/pages/TemplateBuilder): make sidebar steps navigable (#28153)
## What

Makes the remaining `SelectionSummary` sidebar elements clickable jump
targets on `/templates/new/builder`, continuing the work from #27351
(which made module rows navigable).

Clickable now:

| Sidebar element | Jumps to |
|---|---|
| `Base Template` label | `base-infra` |
| Selected base-template row | `base-parameters` (falls back to
`base-infra` when that step is skipped) |
| `Modules` label | `module-select` |
| Each module row | `module-settings` + scroll (already shipped in
#27351) |
| `Customizations` label | `customizations` |

## Back-stack behavior

The sidebar previously colored groups purely from the current step, so
jumping backward would grey out and disable steps you had already
reached. This adds a `maxReachedGroup` that never shrinks on backward
navigation:

- Groups at or below the furthest-reached group stay `complete` (green)
and clickable, like a browser back-stack.
- Groups strictly above render as `upcoming` and inert (no button, no
hover, not focusable).
- The connecting divider color keys off `maxReachedGroup`, not the
current step, so it stays green after navigating backward.

Clickability is gated on `maxReachedGroup` (you can only jump to steps
you have already reached).

## Changes

- `SelectionSummary.tsx`: new required `maxReachedStep` and
`onNavigateStep` props. Split the single `variant()` into
`indicatorVariant` (label circle), `dividerVariant` (connecting line),
and a `reachable()` gate. `StepIndicator` and `BaseTemplateSelection`
render as `<button>` (hover + focus ring, `aria-label`) when a reachable
handler is supplied, else stay inert.
- `TemplateBuilderPageView.tsx`: track `maxReachedGroup`; add
`navigateToStepId(stepId)` that resolves skipped steps via
`nearestVisible` (so `base-parameters` falls back to `base-infra`) and
mirrors the existing customizations reset when leaving that step. Wire
both new props into `SelectionSummary`.
- `SelectionSummary.stories.tsx`: add `onNavigateStep` to meta and
`maxReachedStep` to existing stories; add `NavigationClicks` (asserts
each label/base/module callback), `BackwardNavigation` (dividers stay
green), and `UpcomingStepsInert` (steps above max-reached are not
buttons).

## Out of scope

Everything else from #27077 stays out: gallery height, sensitive-var
banner relocation, trash-icon wiring, and the scroll-past required-field
subsystem.

## Testing

- `pnpm check` (biome) clean
- `pnpm lint:types` (tsc) clean
- `pnpm vitest run --project=storybook src/pages/TemplateBuilder` — 37
pass
- `pnpm vitest run --project=unit src/pages/TemplateBuilder` — 55 pass

<details>
<summary>Implementation plan / decision log</summary>

### Origin

This is the remainder of PR #27077's item #2 (navigable selection
summary), rebased onto current `main` after #27351 shipped the
module-row navigation.

### Why a `maxReachedGroup` back-stack

`furthestAllowedIndex(state)` on current `main` is all-or-nothing (0
without a base selected, otherwise the last step), so it cannot express
"how far the user has progressed" for the sidebar coloring. A monotonic
`maxReachedGroup` (bumped when the current group advances, never shrunk)
is needed to keep completed steps green and clickable after backward
navigation, matching #27077.

### Decisions

- Reachability gating: gate both coloring and clickability on
`maxReachedGroup` (only jump to steps already reached), rather than the
looser `furthestAllowedIndex` (which would let users skip required steps
once a base is chosen).
- Base-template row target: jump to `base-parameters` and let
`nearestVisible` fall back to `base-infra` when the base has no
parameters/prerequisites.
- Module rows: keep `onNavigateModule` passed directly (not re-gated on
reachability), since a module can only be selected after reaching group
2, so `reachable(2)` is always true when module rows render. This
preserves the earlier decision to keep the module row's handler required
with no inert branch.

</details>

---

Coder Agents generated, on behalf of @aqandrew.
2026-08-17 12:15:29 -07:00
TJ b4971bc49f feat(site/src/modules/dashboard/Navbar): replace proxy emoji with latency radio icon in trigger (#28128)
Updates the latency dropdown's collapsed views in the navbar:

- Removes the proxy emoji (`ExternalImage`) from the desktop trigger and
the mobile "Workspace proxy settings" row.
- Shows a lucide `RadioIcon` instead, colored via `getLatencyColor`
(matching the loading state used by the `Latency` component on desktop).
- Keeps the latency text in `content-primary`; only the icon carries the
latency color.

The expanded proxy lists are unchanged (they keep the proxy icon and
colored latency text).

Story changes:

- Added `ClosedWarningLatency` and `ClosedCriticalLatency` to cover the
icon color per latency level.
- Right-aligned the ProxyMenu story trigger to match its navbar
placement, so the end-aligned menu renders without collision shifting in
the story canvas.

> Generated by Coder Agents on behalf of @tracyjohnsonux.
2026-08-17 18:41:27 +00:00
TJ 5d746aa594 fix(site/src): replace hardcoded text-[13px] with scale tokens outside agents (#28071)
Replaces the arbitrary `text-[13px]` value with the design-system tokens
`text-sm` (14px) or `text-xs` (12px) in the 18 non-agents files that
used it. `AgentsPage` usages and `modules/resources/AgentMetadata.tsx`
are intentionally left alone; they'll be handled with the agents UI
separately.

Because the custom Tailwind scale bakes `font-weight: 500` into
`text-xs`/`text-sm`, `font-normal` was added wherever the text
previously rendered at 400 and is prose, code, or log content, so only
the size changes there. Short labels, headers, and numeric values take
the token's 500 weight as-is.

### Token decisions

| File | Token | Rationale |
|---|---|---|
| `components/Logs/LogLine.tsx` | `text-xs font-normal` | Dense mono log
output; 12px keeps line-height close to current |
| `components/PaginationWidget/PaginationAmount.tsx` | `text-xs
font-normal` | Caption-style "showing X of Y" text |
| `pages/IconsPage/IconsPage.tsx` (figcaption) | `text-xs font-normal` |
88px-wide icon captions |
| `pages/WorkspacesPage/WorkspacesButton.tsx` | `text-xs font-normal` |
Secondary line under the template name in the combobox |
| `modules/templates/TemplateExampleCard.tsx`,
`pages/CreateTemplateGalleryPage/...` | `text-xs font-normal` |
Secondary card description prose (and its "Read more" link) |
| `components/FullPageLayout/Sidebar.tsx` / `Topbar.tsx` | `text-sm`
(Topbar adds `font-normal`) | Nav chrome; Topbar is a container so
`font-normal` avoids leaking 500 into all children |
| `components/Paywall/PaywallPremium.tsx` | `text-sm font-normal` |
Feature list prose (compact variant) |
| `modules/templates/TemplateFiles/TemplateFiles.tsx` /
`TemplateFileTree.tsx` | `text-sm` | File headers/tree labels (header
already `font-medium`) |
| `modules/workspaces/WorkspaceOutdatedTooltip.tsx` | `text-sm
font-normal` | Tooltip body prose |
| `pages/WorkspacePage/ResourcesSidebar.tsx` | `text-sm font-normal` |
Help text prose |
| `pages/AISettingsPage/.../CredentialField.tsx` | `text-sm font-normal`
| Mono credential input |
| `pages/DeploymentSettingsPage/Option.tsx` | `text-sm` | Already
`font-semibold` |
| `pages/HealthPage/Content.tsx` | `text-sm font-normal` | Mono detail
block |
| `pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx` |
`text-sm` | "Files" panel header label |
| `pages/TemplatePage/TemplateInsightsPage/TemplateInsightsPage.tsx` |
`text-sm` (`font-normal` on prose/empty state) | Data labels and values
in insight panels |

### Storybook review checklist

Stories directly covering changed components:

- `Logs/LogLine` and `Logs/Logs`
- `Paywall/PaywallPremium`
- `TemplateExampleCard`
- `TemplateFiles` and `TemplateFileTree`
- `WorkspaceOutdatedTooltip`
- `CreateTemplateGalleryPageView`
- `IconsPage`
- `TemplateInsightsPage`
- `TemplateVersionEditor` (also exercises FullPageLayout
`Topbar`/`Sidebar`)

Indirect coverage for components without their own stories:

- `PaginationAmount` → `PaginationWidget/PaginationContainer` stories,
plus paginated page views (`UsersPageView`, `AuditPageView`,
`ConnectionLogPageView`)
- `WorkspacesButton` → `WorkspacesPageView` stories (open the "New
workspace" combobox)
- `HealthPage/Content` → `HealthPage/*Page` stories (`DERPPage`,
`DatabasePage`, etc.)
- `ResourcesSidebar` → `WorkspacePage/Workspace` stories (failed-build
state)
- `FullPageLayout Topbar/Sidebar` → `TemplateVersionEditor` stories
- `CredentialField` → `ProviderForm` / `AddProviderPageView` stories

No story exists for `DeploymentSettingsPage/Option`; verify on the
deployment settings page (option value pills).

---

🤖 This PR was generated by Coder Agents on behalf of @tracyjohnsonux.
2026-08-17 18:41:06 +00:00
Jake Howell 0350bfd2ea refactor(site): show audit log retention and Premium paywall on observability settings (#27947)
The Audit Logging section on deployment observability settings only
showed a badge or an info alert, with no actual setting underneath.

When audit logging is entitled, show the Audit Logs Retention option.
When it is not, show the shared Premium paywall instead of the inline
alert.
2026-08-17 07:19:37 +00:00
Jake Howell 41d2ecec0b feat(site): gate appearance settings behind Premium paywall (#27948)
Appearance settings previously showed a Premium paywall badge above
still-visible branding and announcement banner forms.

When appearance is not entitled, show only the shared Premium paywall.
When entitled, show the branding form and announcement banners.
2026-08-17 07:19:19 +00:00
Jake Howell ea8ba0c678 refactor: remove MUI and Emotion (#27821)
> 🤖 This PR was modified by Coder Agents on behalf of Jake Howell.

Until we meet again.

## Stack

- #27636
- #27718 
- #27719 
- #27722
- #27723
- #27724
- #27728
- #27730
- #27732
- #27762
- #27763
- #27786
- #27787
- #27788
- #27789
- #27790
- #27791 
- #27817 
- #27820
- #28009

## Final removal (`c39b664`)

Removes the last of MUI and Emotion now that every surface has been
migrated:

- **Dependencies**: drops `@mui/material` and
`@emotion/{cache,css,react,styled}` from `package.json` /
`pnpm-lock.yaml`, and deletes the `@types/emotion.d.ts` and
`@types/mui.d.ts` module augmentations.
- **Theming**: replaces the Emotion `CacheProvider`, MUI `ThemeProvider`
/ `StyledEngineProvider`, and `CssBaseline` in `ThemeProvider` with a
lightweight `theme/context.tsx` that exposes `ThemeContextProvider` and
a `useTheme` hook.
- **Global styles**: moves the base `body` styles (background, text
color, font, antialiasing) that `CssBaseline` previously provided into
`index.css`, and drops the temporary MUI modal/popover scrollbar-gutter
workaround.
- **Cleanup**: removes the MUI → shadcn / Emotion → Tailwind migration
guidance from `site/AGENTS.md`, updates the Storybook `preview.tsx`, and
adjusts assorted components (`Command`, `Slider`, `Switch`, `Tabs`,
`SyntaxHighlighter`, timing charts) and theme files to consume the new
context instead of MUI/Emotion.
2026-08-17 14:04:48 +07:00
Michael Suchacz 521c383f6b fix: repair stale chat agent bindings after workspace rebuild (#28152)
## Problem

When a chat is bound to a workspace, chatd persists `chats.agent_id`
pointing at a specific workspace agent, and it only rebinds on the next
chat turn. A workspace stop/start creates a new agent with a new ID in
the latest build, so the chat page resolves the stale agent ID to
`undefined` and the right sidebar silently drops Terminal, Desktop,
Browser, apps, and ports even though the workspace is running. The
existing read-time enrichment only filled nil agent IDs and skipped
stale non-nil ones, so refreshing did not help until the user sent
another message.

## Fix

- `coderd/exp_chats.go`: single-chat reads now repair agent IDs that no
longer resolve in the workspace's latest build, using the same
`agentselect.FindChatAgent` selection chatd uses. A repaired binding
also carries the latest build's ID so the response never pairs the new
agent with the previous build. Bindings that still resolve are
preserved, and repair stays best-effort and response-only (no
write-on-read). List reads keep the previous nil-fill-only behavior
because validating existing bindings would cost a per-workspace
authorization lookup per listed chat.
- `site/src/pages/AgentsPage/AgentChatPage.tsx`: the workspace watch
update handler detects when a running workspace's latest build no longer
contains the chat's bound agent and invalidates the chat query once per
chat/build/binding key for immediate recovery, and the chat query polls
every 30 seconds while the binding remains unresolved so a transiently
failed repair retries even when an idle workspace publishes no further
watch events. The watch stream replays the current workspace on every
(re)connect, so this covers rebuilds that happen while the page is open
or disconnected; page loads are covered by the server-side repair. The
workspace-watcher bailout now also keys on `latest_build.id` so a
rebuild propagates while the page is open.
- `site/src/api/queries/chats.ts`: chat watch events replay the
persisted (pre-repair) binding, so the summary merge adopts a snapshot's
`build_id` only when the snapshot agrees on `agent_id`, keeping the
repaired agent/build pair atomic in the caches.

## Testing

- `go test ./coderd -run TestEnrichChatAgentIDs` covering repair,
keep-valid, selection-error, list-mode-skips-bound, and no-workspaces
cases.
- Storybook interaction story `RecoversSidebarAfterWorkspaceRebuild`
exercising the watch-event to chat-refetch to sidebar-recovery flow
(verified red without the invalidation, green with it).
- `pnpm test AgentChatPage.test.ts` covering the binding-resolution
predicate.

> Mux created this PR on Mike's behalf.
2026-08-16 20:30:32 +02:00
Danielle Maywood 57dc47dc42 feat(site): migrate agent chat scrolling (#28130)
Replace the Agents chat's inverse scroll container and sticky
user-message overlays with the stock MessageScroller from
`@shadcn/react@0.3.0`.

Transcript rows now render as direct MessageScroller Items with stable
server-backed identities. Only the latest active user turn is a scroll
anchor, older history preserves the reading position when prepended, and
the package owns follow mode, prompt navigation, and the `Scroll to
bottom` control. The integration uses the upstream component hierarchy
without patches, bridges, or application-owned scroll correction.

Depends on #28079.

<details>
<summary>Implementation notes</summary>

- Use `MessageScroller.Provider`, `Root`, `Viewport`, `Content`, `Item`,
and `Button` directly.
- Keep durable row IDs stable across pagination; the live assistant uses
an ephemeral row until its durable message arrives.
- Load additional history from MessageScroller's start-edge state,
including underfilled transcripts and retry after a page error.
- Remove inverse scrolling, sticky message copies, scroll refs, forced
scroll commands, and `react-infinite-scroll-component`.

</details>

Generated by Coder Agents on behalf of @DanielleMaywood.
2026-08-14 12:59:19 +01:00
Danielle Maywood 7617b6bdcc refactor(site): render live assistant output as a chat timeline row (#28079)
Refactor the Agents chat timeline so live assistant output renders as a
timeline row through the same components as durable messages, ahead of
the stacked MessageScroller migration in #28130.

`ConversationTimeline`'s block rendering is extracted into
`MessageBlocks`, the streaming/durable assistant split collapses into a
shared `AssistantOutput`, and `LiveStreamTail` shrinks to the empty
state and terminal failure callout. Row keys are plain `message:<id>`
strings; the live assistant row is a separate ephemeral row. This PR
does not change scrolling behavior and adds no backend, API, or database
fields.

<details>
<summary>Implementation notes</summary>

- Extract `BlockList` and friends from `ConversationTimeline` into
`MessageBlocks` (pure move).
- Replace `StreamingOutput` with `AssistantOutput`, used for both live
and durable assistant rows.
- Render the live assistant as a timeline row via `assignTimelineRows`
instead of separate transient content below the transcript.
- Keep existing transcript grouping, prompt navigation, and the current
scroll container unchanged; the scroller swap happens in #28130.

</details>

Generated by Coder Agents on behalf of @DanielleMaywood.
2026-08-14 12:59:18 +01:00
Samuel Volin b26f4c0e03 feat: DEVEX-751 updated premiumpaywall component (#28070)
* implements the supergraphic component, to show a minified webp
theme-aware version of the supergraphic
* refine the PaywallPremium into separate PaywallSmall component
* paywall no long includes link to docs for component; will be part of
DEVEX-742
* appearance and custom roles page uses PaywallSmall

<img width="1509" height="676" alt="Screenshot 2026-08-12 at 7 49 54 AM"
src="https://github.com/user-attachments/assets/17cd1a16-acc8-415a-819d-bdb9259fdaf9"
/>
<img width="1499" height="702" alt="Screenshot 2026-08-11 at 12 46
55 PM"
src="https://github.com/user-attachments/assets/05cd628a-c8c1-4b1c-a4c9-318814eec428"
/>
<img width="1466" height="549" alt="Screenshot 2026-08-11 at 12 44
43 PM"
src="https://github.com/user-attachments/assets/f0bb0c0e-7702-41a0-a718-78a637c9f4e9"
/>
<img width="1715" height="652" alt="Screenshot 2026-08-12 at 11 25
21 AM"
src="https://github.com/user-attachments/assets/55dedd2c-d44c-4cd0-a425-fbde4452f622"
/>
2026-08-13 15:21:22 -06:00
McKayla はな e1fa247e59 feat: redirect to the template builder after first time setup (#27670) 2026-08-13 11:57:29 -06:00
Michael Suchacz bca5d72c1c fix(site): prefer permitted organization for chat creation (#28078)
Fixes first-send 403s for multi-org users who lack `chat:create` in the
deployment default organization (CODAGT-892). Two frontend defects
combined to send chat creation to the wrong organization:

1. The Agents create form initialized its organization selection
permission-blind to the default org and only corrected it when the
`permittedOrganizations` authcheck result *transitioned*, so a fast
first send raced the check, and on warm-query-cache remounts the
correction never ran at all: the wrong org stayed selected permanently
while the org picker was hidden.
2. The permitted-organizations authcheck itself sent no `owner_id`, so
roles that grant `chat:create` at member (owner) scope, such as
`agents-access` (Coder Agents User, the exact role in the customer
report), were denied in every organization and the form always fell back
to the default org. This part was split out and already landed on main
via #28076; after rebasing, this PR relies on that fix and keeps its
stricter regression stories around it.

## Changes

- Derive the effective organization at render time: keep the user's
explicit pick only while it is still permitted, otherwise fall back to
the permitted default org, then the first permitted org, then the
dashboard default. Replaces the transition-based reconciliation, which
could not fire when the query cache was already warm on mount.
- Keep user-driven org cleanup (workspace selection, attachments) in the
picker/dialog event handlers; permission-driven changes are handled by
render-time state adjustments and the attachment hook's post-commit
adoption effect.
- Disable Send until the permitted-organizations check settles and the
attachment hook has adopted the effective org; hide the org picker and
disable the workspace picker until the check settles (their
pre-settlement options come from the unfiltered dashboard fallback, so a
pick could persist a foreign-org workspace).
- Scope persisted attachments to their organization in
`useFileAttachments`: restoration defers until a permitted org is known,
permission-driven org changes replace attachment state post-commit,
in-flight uploads are invalidated by an adoption epoch (including
A-to-B-to-A round trips), and no render exposes another org's file IDs,
including when authorization resolves to no org at all.
- Revalidate org-scoped state on permission refetches: a revoked
explicit selection clears instead of lying latent, a settled
effective-org change drops the stored workspace, and the org-change
confirmation dialog closes (and re-checks on confirm) when its pending
org is revoked.
- Rebase reconciliation with #28076: the stories use its
`permittedOrganizationsKey` helper and retain its
`MemberScopedPermissionsShowOrgPicker` regression story alongside this
PR's stricter member-scope stories.

The backend RBAC rejection was correct; this is frontend-only.

## Testing

- Red-green: every guard above was verified by reverting it and
confirming exactly its guarding story or unit test fails (whole-file
runs).
- `pnpm -C site check`, `pnpm -C site lint`, `pnpm -C site lint:types`,
full `AgentCreateForm.stories.tsx` (42 pass),
`useFileAttachments.test.tsx` (8 pass), re-run after the rebase onto
main.
- Dogfood UAT on a licensed multi-org dev deployment at this branch: a
restricted user with `agents-access` only in a non-default org sends
first and warm-remount messages successfully (201, payload carries the
permitted org, no 403); admin picker, workspace filtering, and
attachment org-change dialog verified. Round 2 of UAT caught the missing
`owner_id` (now landed via #28076); round 3 re-verified end to end.

> Mux created this PR on Mike's behalf.
2026-08-13 18:30:22 +02:00
TJ 49cbd7dde3 fix(site): keep standard avatar border for normal AI spend state (#28037)
The navbar avatar received a grey `border-content-secondary` override
whenever AI spend data was present, so the default (normal spend) state
looked different from a standard avatar. It also relied entirely on
border color to communicate state, which is easy to miss and
inaccessible to colorblind users.

## Changes

- **Avatar border**: always standard — the severity border override is
removed for all states.
- **Warning / exceeded states** get a notification-style corner badge
(like the inbox unread badge):
- Warning: triangle-alert icon on the orange alert surface
(`surface-orange` / `highlight-orange`).
- Exceeded: octagon-alert icon on the red alert surface (`surface-red` /
`highlight-red`).
- Distinct icon shapes per state make the change perceivable without
relying on color; surface/highlight token pairs keep the icon light on
dark mode and dark on light mode.
- **Accessibility**: the trigger now has a descriptive accessible name —
`User menu`, `User menu. AI spend is nearing its limit`, or `User menu.
AI spend limit exceeded`.

Storybook: `AvatarBorderNormal/Warning/Exceeded` stories assert the
trigger's accessible name per state, and Chromatic snapshots cover the
visual states.

<img width="604" height="542" alt="image"
src="https://github.com/user-attachments/assets/3dab5700-c35b-44a6-abc5-7413254adeee"
/>


---
*This PR was generated by Coder Agents on behalf of @tracyjohnsonux.*
2026-08-13 07:20:40 -07:00
Susana Ferreira 2d9b6eda8f feat: add experimental CLI to price unpriced AI models (#27926)
## Description

AI Gateway computes the cost of an interception from `ai_model_prices`,
which is seeded on every server start from a price book embedded in the
binary. A model the price book does not cover records a NULL cost, so
its spend is invisible to cost reporting and is not enforced against
budgets. The only fix was to wait for a Coder release that added the
model.

This adds an experimental CLI, backed by an experimental HTTP endpoint,
for pricing those models. Models the price book already covers are
rejected, because the seeder re-applies the book on every start and
would overwrite an operator price. Support for custom pricing will be
handled in
https://linear.app/codercom/issue/AIGOV-589/extend-experimental-cli-command-to-set-custom-ai-model-prices.

## Commands

```
coder exp ai-model-prices list [--provider] [--model]
coder exp ai-model-prices update [file|-] [--provider] [--model] [--input-price] [--output-price] [--cache-read-price] [--cache-write-price] [--yes]
```

## Changes

- Add `GET` and `POST /api/experimental/ai/model-prices`, gated behind
the AI Bridge entitlement and the existing `ai_model_price` RBAC
resource.
- Add a `GetAIModelPrices` query with optional `provider` and `model`
filters applied in SQL.
- Validate the whole request before writing anything, so one bad entry
cannot leave the table half updated, and report every problem at once.
- Reject prices for models the embedded price book already covers,
through a new `prices.IsDefaultPriced`.
- Add the `coder exp ai-model-prices` command with `list` and `update`.
`update` accepts a JSON document or the single-model flags and prints a
plan, asking to confirm unless the document is piped in or `--yes` is
passed.
- Consolidate the supported provider list into
`coderd/aibridge/prices/providers` so the price generator and the server
share one definition.
- Add `codersdk` types and client methods for both endpoints, and bound
the request body at 1 MiB.
- Document the command in the AI Gateway cost controls page.

Closes
https://linear.app/codercom/issue/AIGOV-567/experimental-cli-command-to-set-prices-for-unpriced-ai-models

> [!NOTE]
> Initially generated by Claude Opus 5, modified and reviewed by
@ssncferreira
2026-08-13 15:00:36 +01:00
Danielle Maywood abe9c79605 feat(site/src/pages/AgentsPage): render list_subagent_models tool results (#28116) 2026-08-13 13:04:54 +01:00
Michael Suchacz d509e1e6a0 fix(site): include owner context in Agents org picker permission check (#28076)
Fixes the Agents org picker and workspace attach menu for users whose
only chat grant comes from the member-scoped "Coder Agents User"
(`agents-access`) org role (PRODUCT-552).

## Problem

The per-org authcheck behind the org picker checked `chat:create` with
only `organization_id`. The `agents-access` role grants chat permissions
at org-member scope, which requires the checked object to be owned by
the caller (`policy.rego` `org_member` requires a non-empty owner
matching the subject). With no `owner_id`, every org check returned
`false`, so:

- the org picker never rendered (`permittedOrgs.length > 1` gate),
- the form stayed pinned to the default org,
- the workspace attach menu, filtered to that org, showed "No workspaces
found" even though the user had workspaces in another org.

The page-level `createChat` check already includes `owner_id: "me"`,
which is why the same user could load the page and create chats via the
API.

## Fix

Pass `owner_id: "me"` in the `permittedOrganizations` seed check in
`AgentCreateForm`, matching the page-level check's semantics. The
`permittedOrganizations` helper spreads the check object through, so
each per-org check now carries owner context and the backend substitutes
the caller's user ID.

The other `permittedOrganizations` callsites
(`organization_member:create`, `template:create`) check org-scoped admin
permissions and correctly omit `owner_id`.

Adds a regression story whose `checkAuthorization` mock only allows
checks carrying `owner_id: "me"` (mirroring the RBAC member-scope
behavior); it fails without the fix and passes with it (red-green
verified).

## Validation

- Red-green verified regression story: fails without the fix (picker not
found), passes with it; all 31 stories in the file pass.
- `pnpm -C site check` and `pnpm -C site lint:types` clean.
- Remote dogfood UAT (dev.coder.com chat
[97be7f39](https://dev.coder.com/agents/97be7f39-4688-45ee-be3d-24bc4f6f8046)):
PASS on all acceptance criteria at this exact commit. Reproduced the bug
scenario end to end (two orgs, non-admin user with only the "Coder
Agents User" role in both, workspace only in the second org): the org
picker renders, the second org's workspaces appear in the attach menu,
and chat creation succeeds with a real model. Single-org and admin
behaviors unchanged. Authcheck probe documents the backend semantics:
`chat:create` with `owner_id: "me"` returns true, without it returns
false.

> Mux acted on Mike's behalf for this PR.
2026-08-13 11:19:38 +02:00
Michael Suchacz 1458d27d78 fix: allow manual chat compaction from the error state (#28022)
A chat that fails generation with a context overflow (for example `Input
length 262625 exceeds the maximum allowed input length of 262112
tokens`) is stuck in a catch-22: `POST /chats/{id}/compact` returns 409
because the `RequestCompaction` transition is only allowed from the
waiting state, and the only other way out of the error state is sending
or editing a message, which re-runs generation with the same oversized
prompt and fails again. Compaction is exactly the recovery a
context-overflowed chat needs, and it is unreachable exactly when it is
needed.

Three semantic changes:

- Allow `RequestCompaction` from the error states: `E0 -> R0` and `E1 ->
R1` (queued messages are preserved and processed after the compaction
turn).
- Clear `last_error` in `Tx.RequestCompaction`, matching the
architecture rule that transitions leaving `E0`/`E1` clear the stored
error. Without this a successful compaction would land in waiting with a
stale persisted error.
- Grant the compaction turn a fresh history epoch: a
`grant_history_epoch` flag on `UpdateChatExecutionState` sets
`history_version = snapshot_version`, resets `generation_attempt`, and
clears `retry_state` in the same atomic update that clears `last_error`
(mirroring the `chat_messages` trigger postcondition). The transition
inserts no history, so without this the turn inherits the failed turn's
spent retry budget, and resetting the counter alone could collide with
message part episode keys still retained on the erroring replica.

No frontend change is required: the chat input is already enabled in the
error state and `/compact` submission already handles both the success
and 409 paths. Also updates ARCHITECTURE.md (transition matrix,
endpoint, and manual compaction sections), the endpoint's swagger
description, and SDK comments.

> Mux created this PR on Mike's behalf.

<!-- mux-attribution: model=claude-sonnet-4-6 thinking=high -->
2026-08-12 20:38:57 +02:00
TJ b145142404 fix(site): add aria-label to icon-only Back navigation links (#26222)
## Summary

Two icon-only "Back" navigation links in the topbars have no accessible
name. The visible tooltip text is wired via Radix's `aria-describedby`,
which provides a *description*, not an accessible **name**, so screen
readers announce them as just "link" with no purpose.

This adds an `aria-label` to each link matching the visible tooltip
text, following the existing `aria-label` convention already used in
these same files (e.g. `aria-label="Create File"`, `aria-label="Daily
usage"`).

## WCAG 2.1 criteria addressed

- **SC 4.1.2 Name, Role, Value** (Level A)
- **SC 2.4.4 Link Purpose (In Context)** (Level A)

## Changes

| File | Element | Added |
|---|---|---|
| `site/src/pages/WorkspacePage/WorkspaceTopbar.tsx` |
Back-to-workspaces chevron link | `aria-label="Back to workspaces"` |
| `site/src/pages/TemplateVersionEditorPage/TemplateVersionEditor.tsx` |
Back-to-template chevron link | `aria-label="Back to the template"` |

No visual change. No behavior change for sighted users. `aria-label` is
safe here because the elements have no visible text content (only an
icon), so it is not overriding a visible name.

<details>
<summary>Why not rely on the existing Tooltip?</summary>

Radix UI's `Tooltip` wires `aria-describedby` from the trigger to the
tooltip content. `aria-describedby` provides an *accessible
description*, not an *accessible name*. WCAG 4.1.2 requires interactive
controls to have a programmatically determinable name, and screen
readers do not consistently announce descriptions, especially when no
name is present. Adding `aria-label` gives the link a stable,
programmatic name that exactly matches the visible tooltip text.

</details>

---

_Created by Coder Agents on behalf of @tracyjohnsonux._
2026-08-12 17:23:37 +00:00
Danielle Maywood 5a33b669b4 feat: redesign the advisor tool row (#28069) 2026-08-12 15:35:31 +01:00
Danielle Maywood c424a76a12 feat: wire chat search box to full-text search (#27973) 2026-08-12 15:04:01 +01:00
Seth Shelnutt 6765731ea9 feat(site/src/pages/AgentsPage): add download and export for personal skills (#28032)
## Summary

Adds a way to get personal skills back out of Coder Agents as files, so
sharing a skill no longer means pasting `SKILL.md` by hand.

- **Per-skill Download**: a `Download` action on each row of the
*Personal
  skills* settings page saves that skill's `SKILL.md` as `<name>.md`.
- **Export all**: a header button zips every personal skill (each as
  `<name>/SKILL.md`) and downloads `personal-skills.zip`.

Scope is **personal skills only** (workspace/filesystem skills are
read-only
in chat and out of scope). No backend changes: the single-skill content
endpoint (`GET /api/experimental/users/{user}/skills/{skillName}`)
already
returns full content, so the view fetches on demand and downloads with
`file-saver`, zipping with `jszip` (both existing deps, matching
`DownloadLogsDialog`).

## Changes

- `AgentSettingsPersonalSkillsPageView.tsx`: `Download` per-row button
(with
per-row spinner) and an `Export all` header button (disabled when empty
or
  loading).
- `AgentSettingsPersonalSkillsPage.tsx`: container handlers that fetch
  content via `queryClient.fetchQuery(userSkill(name))` and trigger the
download/zip, with `toast` error handling. Download logic is extracted
to
  module-level helpers to stay React Compiler friendly.
- `AgentSettingsPersonalSkillsPageView.stories.tsx`: interaction stories
  asserting `onDownload`/`onExportAll` fire, plus loading-state stories.

## Testing

- `pnpm check` (biome), `pnpm lint:types` (tsc), React Compiler check,
and
  Storybook interaction tests (22 passed) all pass locally.
- `make pre-commit` passed.

Closes
[CODAGT-918](https://linear.app/codercom/issue/CODAGT-918/add-ability-to-download-and-export-skills-from-coder-agents).

<details>
<summary>Implementation plan</summary>

### Problem

Users can create, edit, and delete personal skills in Agent settings,
but
there is no way to get a skill back out as a file. The only workaround
was to
paste the `SKILL.md` content by hand.

### Decisions (confirmed with requester)

1. Surface: download in the settings UI (Personal skills page).
2. Build both single-skill download and export-all (zip).
3. Personal skills only (workspace/filesystem skills out of scope).

### Approach (frontend-only, additive)

No backend changes: the single-skill content endpoint already exists.
The
view stays presentational and exposes new callbacks; the container
fetches
content and performs the download, mirroring how Edit/Delete already
split
between view and container.

- Per-row Download button, placed before Edit in the actions cell; shows
a
  spinner while its own row is downloading.
- Export all button in the section header, disabled when there are no
skills
  or while loading.
- Container: single download fetches content and `saveAs(<name>.md)`;
export
  all fetches every skill, adds each as `<name>/SKILL.md` to a `JSZip`,
generates a blob, and `saveAs(personal-skills.zip)`. Failures surface
via
  `toast.error`.
- Stories cover the interactions and loading states (stories are the FE
test
  surface).

### Out of scope

- Workspace skills download (filesystem source): different data path,
  read-only in chat, not user-owned data.
- In-chat download button on the `read_skill` tool output: possible
  follow-up, different surface and interaction model.

</details>

---

_Opened by Coder Agents on behalf of @Shelnutt2._
2026-08-12 09:20:30 -04:00
Michael Suchacz ac11ae52d1 fix(site/src/pages/AgentsPage): stop attachment downloads from trapping iOS PWAs (#27853)
On iOS, tapping an attachment download link inside the installed
(standalone) PWA hands the file to QuickLook, which renders on top of
the app with no reliable way back. Users had to force-quit the PWA to
recover.

## Approach

In iOS standalone mode, when the device supports file sharing, the
download click is intercepted and the fetched attachment is handed to
the native share sheet (`navigator.share` with a `File`), where the user
can pick Save to Files or any other target. This is the
platform-intended path for saving files from a PWA; no library covers it
(FileSaver.js is broken in iOS PWAs, and browser-fs-access falls back to
the same broken anchor path).

Everything else keeps native behavior:

- Non-iOS platforms and regular iOS Safari tabs keep the plain `<a
download>` anchor.
- iOS standalone devices without file sharing keep the anchor too.
- Inline `data:` attachments are decoded locally because the production
CSP (`connect-src 'self'`) blocks fetching them.

## Error model

Deliberately small: a share-sheet dismissal (`AbortError`) is silent, an
expired click gesture (`NotAllowedError`, which happens when the fetch
outlasts transient activation) shows a toast whose Save action retries
with a fresh gesture, and any other failure shows a plain error toast.

An earlier revision carried pending-state tracking, abort-signal
threading through unmount, and a blob-URL tab fallback; those guards
were removed on purpose to keep the change proportionate to the defect.

Also hardens chat file uploads slightly: filenames are sanitized before
upload and the file-picker `accept` attribute mirrors the server's
attachment allowlist.

## Testing

- Storybook interaction tests cover the share happy path and
native-anchor behavior; unit tests cover the intercept gate (including
iPadOS detection), failure toasts, retry action, and inline-data decode.
- Validated on an iPhone against a dev deployment: share sheet opens,
Save to Files works, and the app remains usable afterwards.

> Mux implemented this on Mike's behalf.

<!-- mux-attribution: model=claude-sonnet-4-5 thinking=high -->
2026-08-12 08:31:26 +02:00
Jake Howell 726e86cef7 refactor(site): add <Drawer /> and migrate build logs drawer (#28009)
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.

Follow-up to #27798. That PR introduced a shared `Drawer` primitive by
adding [`vaul`](https://github.com/emilkowalski/vaul) as a dependency.
As raised in review, `vaul` is currently unmaintained, and this drawer
is the only place in the UI using that concept.

This PR takes the
[suggested](https://github.com/coder/coder/pull/27798#pullrequestreview-4901095865)
route: a dead-simple, self-owned drawer that covers this one case well,
built directly on Radix UI's `Dialog` (already a dependency via
`radix-ui`) instead of `vaul`. It keeps the same shadcn-style API
(`Drawer`, `DrawerTrigger`, `DrawerContent`, `DrawerHeader`,
`DrawerFooter`, `DrawerTitle`, `DrawerDescription`, `DrawerClose`) so
usage stays familiar, and migrates `CreateTemplatePage`'s
`BuildLogsDrawer` off MUI onto it.

### What changed

- Add `site/src/components/Drawer/Drawer.tsx`: a reusable drawer/sheet
built on `radix-ui` `Dialog`. Supports a `direction` prop
(`top`/`bottom`/`left`/`right`, default `right`) with slide animations
via `tailwindcss-animate`. No new dependencies.
- Migrate `BuildLogsDrawer` from `@mui/material/Drawer` to the new
component. Desktop panel stays at 800px via `min(800px, 100%)` so it
stays within the viewport on mobile.
- Storybook coverage with `play()` functions for both the generic
`Drawer` (open/close, direction) and `BuildLogsDrawer` (close via the X
button and via Escape assert `onClose`), addressing the earlier P1
review note about covering the controlled close path.

### Why Radix instead of vaul

- `radix-ui` `Dialog` is already a dependency and provides
accessibility, focus management, and open/close state.
- No dependency on an unmaintained package for a single-use concept.
- `vaul`'s drag-to-dismiss gesture is not needed for this build-logs use
case.
2026-08-12 13:51:04 +10:00
TJ 5fab238ed1 fix(site/src/pages/AgentsPage): improve git panel color contrast (#28038)
## Summary

Follow-up to #27012 addressing color contrast in the git panel:

- The orange (`text-content-warning`) **Working** label failed color
contrast. The label in the view switcher trigger and dropdown now uses
`text-content-secondary`. The `CircleDotIcon` keeps its original
`text-content-warning` color as a state indicator.
- The **Commit** and **View PR** buttons now use `text-content-primary`
for their text and icons instead of `text-content-secondary`. The
now-redundant `hover:text-content-primary` was dropped.

No behavior changes; class-only updates in `GitPanel.tsx` and
`RemoteDiffPanel.tsx`.

## Testing

- `biome check` and `tsc --noEmit` pass on the touched files.
- Verified visually via Storybook screenshots of the GitPanel stories.

---

> Generated by Coder Agents on behalf of @tracyjohnsonux.
2026-08-11 14:04:15 -07:00
TJ f7f840ac69 feat(site/src/pages/AgentsPage): consolidate git panel tab strip into a view switcher dropdown (#27012)
## What

Replaces the git panel's tab strip (`[PR #4847] [Working coder] [Working
other]`) with a single dropdown "view switcher" that lists the PR (when
present) and each dirty working repo as items.

Also:

- Moves the PR title to a dedicated row below the switcher, truncated
with a hover tooltip for the full title. The row only appears while the
PR view is active.
- Removes the redundant PR-state badge from the RemoteDiffPanel
sub-header (state is now shown as the colored pill on the switcher
trigger).

## Why

The common multi-view states are `PR only`, `Working only`, or `PR + one
Working repo`. The tab strip was fine for one view but crowded the
toolbar the moment two showed up. Collapsing to a single trigger +
optional dropdown keeps the toolbar quiet in the 80% case and the
switcher stays intuitive when there is more than one thing to look at.

## Screens

Storybook coverage:

- `PullRequestAndWorkingChanges` — collapsed switcher shows the PR state
+ `PR #<n>`, PR title below.
- `ViewSwitcherOpen` (new) — dropdown open with PR and both working
repos, then clicks a working entry to verify the view swaps.
- `DraftPullRequest`, `MergedPullRequest`, `ClosedPullRequest` —
trigger's colored pill reflects state (play assertions added).
- `WorkingChangesOnly`, `BranchOnly`, `MultipleRepos`, `EmptyState`,
`GitNotActive`, `GitStatusLoading` — all pass.
- `EverDirtyRepoGoneClean`, `CleanRepoFromStart` — regression coverage
updated to target the new switcher rather than the old `Working` tab
buttons.

<details>
<summary>Decisions and scoping notes</summary>

### Why a switcher and not a PR-only dropdown

Original design showed multiple PRs in the dropdown, but the server
model only tracks **one PR per chat** (`ChatDiffStatus.pr_number` is a
single field). Multi-PR would need a DB / watcher / SDK change out of
scope for a UI polish PR.

After reviewing frequency of the actual multi-view states (`PR only` and
`Working only` dominate; `PR + Working` shows up during review
iteration; 3+ views is rare), we scaled back to make the dropdown a
**view switcher** that spans the existing tab strip's data: PR + working
repos. Ships today, no backend work.

### Behavior notes

- With a single item the trigger renders as a static pill (no chevron,
no dropdown behavior); it becomes an interactive dropdown once there is
more than one item.
- The colored state pill on the trigger uses `text-git-*-bright` /
`text-content-warning` / `text-content-secondary` (text color only; no
background token).
- Dropdown items render as `[icon] primary secondary` where
primary/secondary is `PR #<n> / title` for the remote entry and `Working
/ repo` for local entries.
- Local (Working) entries are visually nested under the remote/PR entry
when one is present; otherwise they render flush-left.
- When the head branch is known but no PR is opened, the trigger reads
`Branch | <head-branch>`.

</details>

---

_Coder Agent generated on behalf of @tracyjohnsonux._
2026-08-11 12:25:06 -07:00
Jeremy Ruppel e9ee83af69 fix(site/src): gate TemplateExampleCard Use template link on builder state (#28035) 2026-08-11 14:30:37 -04:00
Andrew Aquino b721935fc1 chore(site): add generateConnectionSessionId helper function (#27935)
- adds a `generateConnectionSessionId` helper function which reuses our
existing `generateRandomString` logic
- adds tests for `generateConnectionSessionId` in utils/random.test.ts
(utils/random.ts was previously untested)
- renames our existing `generateRandomString` function to be
`generateRandomBase64String`

additional context in
https://github.com/coder/coder/pull/27671#discussion_r3731213459

This PR doesn't change any of our UIs. The new
`generateConnectionSessionId` function is a piece of frontend plumbing
related to DEVEX-663. I originally implemented #27677 so that web
terminal connections to workspaces would be identified by uuids--but
according to the RFC for DEVEX-663, those connection session ids should
be lowercase hexadecimal strings (not uuids).
2026-08-11 09:38:28 -07:00
Atif Ali d7953bd046 fix(coderd): use service account wording in account notifications (#27536) 2026-08-11 13:16:25 +00:00
Michael Suchacz 57f38b5c24 fix: keep chat attachments while a linking chat exists
Fixes https://linear.app/codercom/issue/CODAGT-616/keep-chat-attachments-while-chats-remain-unarchived

Chat attachments could disappear even though the chat was still available. This happened when a message was saved without recording which attachments it used, or when cleanup deleted attachments before an archived chat itself was removed.

Creating a chat, sending or queuing a message, and editing a message now record both the message and which attachments it uses as one operation. If the chat is already at the 50-attachment limit, the chat change fails without being partially saved.

Concurrent attachment writes serialize the 50-file cap per chat. Cleanup locks candidates and checks again for new links before deleting. If a file becomes unavailable after input validation, create, send, and edit return a clear client error and roll back the chat change.

An attachment stays available while any chat that uses it still exists. After an archived chat reaches the end of its retention period and is deleted, an old attachment that no remaining chat uses can be cleaned up. The retention guide and unavailable-attachment UI text document this lifecycle. This change cannot restore attachments that were already deleted.

The database migration adds two indexes so attachment cleanup stays fast as attachments accumulate.

> This PR was authored by Mux (AI) on Mike's behalf.
2026-08-11 13:53:15 +02:00
Danielle Maywood df278ec079 fix: derive diff cache keys from patch content (#27987) 2026-08-11 12:22:13 +01:00
Michael Suchacz 2e5353bde7 feat: add built-in Browser tab for agent-browser (#27910)
Adds a built-in Browser tab to the Agents page right panel, alongside
the built-in Terminal and Desktop tabs, when the chat's bound agent has
an app with the well-known slug `agent-browser`. The tab shows only
while the app is embeddable and its health is `healthy` (or `disabled`,
for templates without a healthcheck), so it appears and disappears live
as the daemon comes up or goes down. The iframe stays mounted across tab
switches to preserve session state.

To avoid duplicates, the generic Add Tab menu and persisted
workspace-app tabs now exclude the `agent-browser` app. Detection uses
the existing `coder_app` slug and healthcheck signals already present in
the workspace data model. The workspace watch handler compares the agent
app fields the chat UI consumes, so health transitions propagate without
re-render churn on every heartbeat.

On the backend, the chat `execute` tool now exports
`AGENT_BROWSER_SESSION=<chat id>` on every process it starts.
agent-browser resolves its default session from that variable, so
browser automation from each chat lands in its own isolated session
(named by the chat id in the embedded dashboard) instead of a shared
default browser.

> Mux created this PR on Mike's behalf.
2026-08-11 12:49:12 +02:00
Thomas Kosiewski 496ed757ed fix: prevent IP leaks via external chat images (#27362) 2026-08-11 11:50:56 +02:00
Jake Howell b3acf7f75a refactor(site): migrate organization settings form off MUI (#27718)
> 🤖 This PR was modified by Coder Agents on behalf of Jake Howell.

Migrates the organization settings info form from MUI TextField to
shared FormField/Input/Textarea patterns, and splits the page view into
focused section components so info, workspace sharing, and delete are no
longer one monolithic file.

Behavior is unchanged; this is structure and component migration only.

- Replace MUI TextField with FormField for slug and display name, plus a
Textarea for description
- Extract OrganizationInfoForm, WorkspaceSharingSection, and
DeleteOrganizationSection
- Move sharing dialog Storybook coverage onto WorkspaceSharingSection
2026-08-11 01:35:12 +00:00
McKayla はな efd5ce3668 test: fix failing storybook stories (#27674) 2026-08-10 17:03:28 -06:00
Andrew Aquino 87fdd2b59b feat(site): add server-side search and pagination for groups page (#27994)
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.
2026-08-10 13:23:14 -07:00
Andrew Aquino 6e07e2610f feat: add paginated API endpoint for groups (#27603)
backend-only changes from #27271; see that PR for summary of changes +
implementation details
2026-08-10 13:23:14 -07:00
McKayla はな ddb2799009 fix: hide the favorite button for workspaces you don't own (#28000) 2026-08-10 14:15:57 -06:00
McKayla はな 09b0d8e108 chore: update stale Chromatic comments (#27714) 2026-08-10 14:15:28 -06:00
McKayla はな b29af7f64a fix: improve layout of CLI install page (#27998) 2026-08-10 14:03:04 -06:00
Jeremy Ruppel baa1e3a6c5 fix(site): make delete dialog confirmation text use primary color (#28002)
## 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._
2026-08-10 15:57:51 -04:00
Susana Ferreira 27414788f7 chore: use 'AI spend' and 'approximate' in budget UI (#27977)
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
2026-08-10 16:08:06 +01:00
Thomas Kosiewski 84f4a8bb17 fix(site/src): prevent protocol-relative login redirects (#27363) 2026-08-10 15:35:06 +02:00
Steven Masley 9a57dfa642 feat: include agent metadata in workspace list responses (#27934)
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.
2026-08-10 08:13:32 -05:00
Matt Vollmer a3a51228ee chore(site): remove Beta tag from agents navigation (#27986)
## 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
2026-08-10 08:44:56 -04:00
Danielle Maywood 192842c8f2 fix(site/src): treat chat deleted watch events as archive instead of eviction (#27921) 2026-08-10 09:29:38 +01:00
Danielle Maywood 19fdc23d63 fix(site/src): fan chat message upserts out to every containing page (#27912) 2026-08-10 09:08:05 +01:00
Jake Howell bab9963a9a fix(site): defer external app API key generation to on-click (#27907)
> 🤖 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.
2026-08-10 14:44:35 +10:00