Commit Graph
4737 Commits
Author SHA1 Message Date
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
Jake Howell b2cfc4e530 refactor(site): move Add users into <SettingsHeader /> (#27906)
Move the Add users action into SettingsHeader on the organization
Members page and group detail page, matching the Users page layout. It
no longer sits inline with the filters.

| Old | New |
| --- | --- |
| <img width="2934" height="1742" alt="ORGANIZATION_MEMBERS_OLD"
src="https://github.com/user-attachments/assets/c09ccb0b-cce5-4b95-a63b-40c42dd2e1ac"
/> | <img width="2934" height="1742" alt="ORGANIZATION_MEMBERS_NEW"
src="https://github.com/user-attachments/assets/a20089b7-04f0-47dc-8153-8f5cca8999cf"
/> |
2026-08-08 18:03:43 +10:00
Jake Howell ad225269aa refactor(site): redesign <AppearanceSettingsPage /> (#27722)
> 🤖 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"/>
|
2026-08-08 18:03:31 +10:00
Jake Howell 7a9b4250aa refactor(site): demui <IconField /> (#27719)
> 🤖 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.
2026-08-07 09:56:05 +10:00
Samuel Volin 50640063a2 feat: DEVEX-732 premium badging (#27847)
Premium badging and gating consistency

as a OSS user, I want to be upsold to premium, and tastefully

Summary

Standardize all base-Premium full-page gates and the two named inline
notices. Admins see an in-app “Learn about Premium” path; non-admins are
told to contact their deployment administrator.
* DEVEX-732
* updates for premium docs pages for consistency
* updates for premium badging and paywall components
* updates implemented uses of premium badge and premiumpaywall


| Before | After |
| --- | ----------- |
| <img width="1271" height="564" alt="Screenshot 2026-08-04 at 3 02
32 PM"
src="https://github.com/user-attachments/assets/027d4bca-3e34-40b2-ad69-28dbaa4a004b"
/> | <img width="1273" height="600" alt="Screenshot 2026-08-04 at 3 26
37 PM"
src="https://github.com/user-attachments/assets/321083c0-7a4c-4c7e-a19c-059807018d3b"
/> |

| Before | After |
| --- | ----------- |
| <img width="1084" height="672" alt="image"
src="https://github.com/user-attachments/assets/741e6bd9-93b0-4ae0-97df-027e8aba5716"
/> | <img width="1289" height="622" alt="Screenshot 2026-08-04 at 3 20
07 PM"
src="https://github.com/user-attachments/assets/b3a8c169-ca6e-439b-8752-9209131fc097"
/> |

| Before | After |
| --- | ----------- |
| <img width="1091" height="865" alt="image (1)"
src="https://github.com/user-attachments/assets/f7cd92dd-a975-4db0-bc2a-af092ba783ce"
/> | <img width="1268" height="680" alt="Screenshot 2026-08-04 at 3 37
06 PM"
src="https://github.com/user-attachments/assets/8af8081a-0ec9-4fd3-921c-470127f2328b"
/> |
2026-08-06 12:13:12 -06:00
Andrew Aquino baed1455cf feat(site/src/pages/TemplateBuilder): scroll to module when clicking sidebar row (#27351)
## 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.
2026-08-06 08:26:49 -07:00
Danielle Maywood f9047e5836 feat(site/src): reconcile chats-by-workspace cache across archive and watch paths (#27901)
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.
2026-08-06 10:25:37 +01:00
Danielle Maywood 632eecc090 feat(site/src): reintroduce chat search cache invalidation (#27892)
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.
2026-08-06 10:25:36 +01:00