Commit Graph
4241 Commits
Author SHA1 Message Date
Kyle Carberry 2bdacae5f5 feat(chatd): add LLM stream retry with exponential backoff (#22418)
## Summary

Adds automatic retry with exponential backoff for transient LLM errors
during chat streaming and title generation. Inspired by
[coder/mux](https://github.com/coder/mux)'s retry mechanism.

## Key Behaviors

- **Infinite retries** with exponential backoff: 1s → 2s → 4s → ... →
60s cap
- **Deterministic delays** (no jitter)
- **Error classification**: retryable (429, 5xx, overloaded, rate limit,
network errors) vs non-retryable (auth, quota, context exceeded, model
not found, canceled)
- **Retry status published to SSE stream** so frontend can show
"Retrying in Xs..." UI
- **Title generation** retries silently (best-effort, nil onRetry
callback)

## New Package: `coderd/chatd/chatretry/`

| File | Purpose |
|------|---------|
| `classify.go` | `IsRetryable(err)` and `StatusCodeRetryable(code)` |
| `backoff.go` | `Delay(attempt)` — exponential doubling with 60s cap |
| `retry.go` | `Retry(ctx, fn, onRetry)` — infinite loop with
context-aware timer |

## Test Helpers: `coderd/chatd/chattest/errors.go`

Anthropic and OpenAI error response builders for use in chattest
providers:
- `AnthropicErrorResponse()`, `AnthropicOverloadedResponse()`,
`AnthropicRateLimitResponse()`
- `OpenAIErrorResponse()`, `OpenAIRateLimitResponse()`,
`OpenAIServerErrorResponse()`

## SDK Changes: `codersdk/chats.go`

- New `ChatStreamEventType: "retry"`
- New `ChatStreamRetry` struct with `Attempt`, `DelayMs`, `Error`,
`RetryingAt` fields
- TypeScript types auto-generated

## Changed Files

- `coderd/chatd/chatloop/chatloop.go` — wraps `agent.Stream()` in
`chatretry.Retry()`
- `coderd/chatd/chatd.go` — publishes retry events to SSE stream with
logging
- `coderd/chatd/title.go` — wraps `model.Generate()` in silent retry
- `coderd/chatd/chattest/anthropic.go` / `openai.go` — error injection
support

## Tests

42 tests covering classification (33), backoff (9), and retry scenarios
(8).
2026-02-27 18:34:33 -05:00
Kyle Carberry 4b5ec8a9a4 feat: add diff_status_change event to /chats/watch pubsub stream (#22419)
## Summary

Adds a new `diff_status_change` event kind to the `/chats/watch` pubsub
stream so the sidebar can update diff status (PR created, files changed,
branch info) without a full page reload.

### Problem

When a chat's diff status changes (e.g. PR created via GitHub, git
branch pushed), the sidebar didn't update because:
1. The backend `publishChatPubsubEvent` didn't include diff status data
2. The frontend watch handler only merged `status`, `title`, and
`updated_at` from events

### Solution

A **notify-only** approach: a new `ChatEventKindDiffStatusChange` event
kind tells the frontend "diff status changed for chat X" — the frontend
then invalidates the relevant React Query cache entries to re-fetch.

### Backend changes

- **`coderd/pubsub/chatevent.go`**: New `ChatEventKindDiffStatusChange =
"diff_status_change"` constant
- **`coderd/chatd/chatd.go`**: New `PublishDiffStatusChange(ctx,
chatID)` method on `Server`
- **`coderd/chats.go`**: New `publishChatDiffStatusEvent` helper.
Published from:
- `refreshWorkspaceChatDiffStatuses` — after each chat's diff status is
refreshed via GitHub API
- `storeChatGitRef` — after persisting git branch/origin info from
workspace agent

### Frontend changes

- **`AgentsPage.tsx`**: Handle `diff_status_change` event by
invalidating `chatDiffStatusKey` and `chatDiffContentsKey` queries
- **`ChatContext.ts`**: Remove redundant diff status invalidation that
fired on every chat status change (the new event kind handles this
properly)
2026-02-27 18:06:54 -05:00
Kyle Carberry b0c6a6dc25 fix(site): optimistic message on agent chat submit (#22422)
## Problem

When sending a message in the agent detail chat, the text lingered in
the input textarea while the HTTP POST round-tripped to the server. Only
after the server responded did the input clear and the message appear in
the timeline (via WebSocket). This created a noticeable delay where the
user couldn't start typing their next message.

## Solution

**Optimistic input clear** (`AgentChatInput.tsx`):
- Clear the textarea and editing state *immediately* on submit, before
awaiting the network call.
- Capture the input text beforehand so it can be restored in the `catch`
block if the request fails.

**Optimistic user bubble** (`AgentDetail.tsx`):
- Inject a temporary `ChatMessage` (with a negative ID) into the chat
store so the user's message bubble appears in the timeline instantly.
- Set chat status to `pending` and clear stream state, mirroring the
existing edit-message path.
- On error, roll back: remove the optimistic message and restore the
previous chat status.

The real message arrives via the WebSocket stream and
`upsertDurableMessage` replaces the optimistic entry naturally (the
server message has a positive ID, so it's inserted alongside; the
optimistic negative-ID message gets cleaned up when `replaceMessages` is
called with the authoritative message list from the next query
invalidation).

## Testing

- Type a message and press Enter — input clears and bubble appears
immediately.
- Simulate a network error — input text is restored, optimistic bubble
is removed.
- Edit an existing message — unchanged behavior (already had optimistic
updates).
- Queue a message while streaming — unchanged behavior.
2026-02-27 17:49:53 -05:00
Kyle Carberry 5fb644a6cd feat(site): add keyboard shortcuts to agents page (#22417)
Adds two keyboard shortcuts to the agents page:

- **Escape** — Interrupts the running agent when viewing a chat detail
page. Only fires when focus is outside text inputs/textareas so it
doesn't conflict with the existing edit-cancel Escape handler in the
chat input.
- **Ctrl+N / Cmd+N** — Navigates to create a new agent. Also skipped
when focus is in a text input/textarea.

Both keybindings are implemented in a new `useAgentsPageKeybindings.ts`
hook file:
- `useAgentsPageKeybindings` — used in `AgentsPage.tsx` for Ctrl+N
- `useAgentDetailKeybindings` — used in `AgentDetail.tsx` for Escape →
interrupt
2026-02-27 17:33:43 -05:00
Kyle Carberry 12083441e0 feat(chats): archive chats instead of hard-deleting them (#22406)
## Summary

The UI has always labeled the action as "Archive agent" but the backend
was performing a hard `DELETE`, permanently destroying chats and all
their messages.

This change replaces the hard delete with a soft archive, consistent
with the pattern used by template versions.

## Changes

### Database
- **Migration 000423**: Add `archived boolean DEFAULT false NOT NULL`
column to `chats` table
- Replace `DeleteChatByID` query with `ArchiveChatByID` (`UPDATE SET
archived = true`)
- Add `UnarchiveChatByID` query (`UPDATE SET archived = false`)
- Filter archived chats from `GetChatsByOwnerID` (`WHERE archived =
false`)

### API
- Remove `DELETE /api/experimental/chats/{chat}`
- Add `POST /api/experimental/chats/{chat}/archive` — archives a chat
and all its descendants
- Add `POST /api/experimental/chats/{chat}/unarchive` — unarchives a
single chat (API only, no UI yet)

### Backend
- `archiveChatTree()` recursively archives child chats (replaces
`deleteChatTree()` which hard-deleted)
- Chat daemon's `ArchiveChat()` archives the full chat tree in a
transaction
- Authorization uses `ActionUpdate` instead of `ActionDelete`

### SDK
- Replace `DeleteChat()` with `ArchiveChat()` and `UnarchiveChat()`
- Add `Archived` field to `Chat` struct

### Frontend
- `archiveChat` API call uses `POST .../archive` instead of `DELETE`
- No UI changes — the "Archive agent" button now actually archives
instead of deleting

## Design Decision

This follows the **template version archive pattern** (Pattern B in the
codebase):
- `archived boolean` column (not `deleted boolean`)
- Dedicated `POST .../archive` and `POST .../unarchive` routes (not
repurposing `DELETE`)
- Reversible — users can unarchive via the API (UI for this will come
later)
2026-02-27 16:46:19 -05:00
blinkagent[bot]andblink-so[bot] 8bb80b060e fix(e2e): fix flaky verifyParameters assertion in updateWorkspace test (#22413)
## Problem

The `update workspace, new required, mutable parameter added` e2e test
has been flaking consistently
([internal#1328](https://github.com/coder/internal/issues/1328)). The
error:

```
Error: Timed out 5000ms waiting for expect(locator).toHaveValue(expected)
Locator: getByTestId('parameter-field-Sixth parameter').locator('input')
Expected string: "99"
Received string: ""
```

## Root Cause

A race between page navigation and data hydration in `verifyParameters`:

1. The page navigates with `waitUntil: "domcontentloaded"` which does
not wait for API responses to settle
2. React Query may serve stale cached workspace data initially (from
before the update), causing the form to render with empty/old parameter
values
3. The `toHaveValue` assertion uses the default `actionTimeout` of
5000ms which isn't enough time for fresh data to arrive and the form to
re-render

## Fix

- Switch `verifyParameters` navigation to `waitUntil: "networkidle"` to
ensure API responses (workspace data, build parameters) are settled
before the form renders
- Increase the `toHaveValue` timeout to 15s to handle cases where
dynamic parameters hydrate slowly after initial render

Fixes coder/internal#1328

---------

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
2026-02-27 16:32:16 -05:00
Kyle Carberry 1a87e74574 fix(site): prevent race conditions when switching chats on agents page (#22404)
## Problem

When switching between chats on the agents page, stream parts could be
lost or applied to the wrong chat due to several race conditions in
`ChatContext.ts`:

1. **`startTransition` deferred parts escape cleanup** —
`startTransition(() => store.applyMessageParts(parts))` defers the state
update. If a chat switch happens between `flushMessageParts` being
called and the transition executing, old-chat parts could apply after
`resetTransientState()` has already cleared stream state for the new
chat.

2. **`message` event has no `chat_id` filter** — Unlike `message_part`,
`queue_update`, and `status` events, the `message` event handler did not
check `streamEvent.chat_id`. While the server-scoped WebSocket makes
this safe in practice, it's an inconsistency in defensive programming.

3. **Brief stale message window on switch** — Between `chatID` changing
and `replaceMessages()` firing (after the query resolves), the store
held old-chat messages while the new WebSocket was already connected.

## Changes

### `ChatContext.ts`
- Added `activeChatIDRef` to track the currently active chat ID
- Guard `startTransition` callback: check `activeChatIDRef` before
applying message parts, discarding them if the chat has switched
- Added `chat_id` filter to `message` event handler, matching the
pattern used by all other event types
- Added `store.replaceMessages([])` to the chatID-change effect so
messages are cleared immediately on switch

### `ChatContext.test.tsx`
Four new tests covering the chat-switch lifecycle:
- WebSocket closure and state reset when chatID changes
- `message` event filtering by `chat_id`
- `startTransition` deferred parts discarded after switch
- Messages cleared immediately before new query resolves

All 13 tests pass (8 existing + 4 new + 1 existing).
2026-02-27 15:33:16 -05:00
Danielle Maywood 7e8559aac0 fix: respect light/dark theme in agents FilesChangedPanel diff viewer (#22403)
## Problem

The git diff on the `/agents` page had color issues: the editor
background followed light mode but the syntax highlighting used dark
mode (`github-dark-high-contrast`), and the filename header used
light-colored text on a light background.

The root cause was hardcoded dark theme options in the `FileDiff`
component:

```tsx
themeType: "dark",
theme: "github-dark-high-contrast",
```

## Fix

Uses the same theme-aware pattern as every other diff/file viewer in the
codebase (`WriteFileTool`, `EditFilesTool`, `ReadFileTool`, `Tool`,
`response.tsx`):

1. `useTheme()` from `@emotion/react` to read `palette.mode`
2. `getDiffViewerOptions(isDark)` from the shared `utils.ts` module —
returns `github-light` theme for light mode, `github-dark-high-contrast`
for dark mode
3. Reuses `DIFFS_FONT_STYLE` and `diffViewerCSS` constants instead of
inlining duplicates

## Storybook coverage

Added four new stories with real unified diff content:
- **WithDiffDark** — dark mode with a PR link
- **WithDiffLight** — light mode with a PR link
- **NoPullRequestDark** — dark mode, "Files Changed" header
- **NoPullRequestLight** — light mode, "Files Changed" header

The existing stories only covered empty and parse-error states with no
rendered diff.
2026-02-27 20:13:41 +00:00
Kyle Carberry d4cfb24a4a fix: update document title when switching agents on the agents page (#22401)
When navigating to a specific agent on the Agents page, the browser tab
title now reflects the agent's chat title (e.g. `Fix login bug - Agents
- Coder`). When the title hasn't loaded yet or when navigating away, it
falls back to `Agents - Coder`.

**Changes:**
- Added a `useEffect` in `AgentDetail` that sets `document.title` via
the existing `pageTitle` utility whenever the chat title changes.
- The cleanup function resets the title back to `Agents - Coder` when
unmounting (navigating away from the agent).
2026-02-27 14:26:28 -05:00
Kyle Carberry 7043e773cf fix: auto-scroll to bottom when switching chats on agents page (#22397)
When switching between chats on the agents page, the scroll position was
preserved from the previous chat instead of resetting to show the most
recent messages.

## Problem
Clicking a different chat in the sidebar loaded the new chat's messages
but kept the scroll container at whatever position the user had scrolled
to in the previous chat. This meant users often landed in the middle of
a conversation instead of at the bottom where the latest messages are.

## Fix
Added a `useEffect` in `AgentDetail` that resets `scrollTop` to `0`
whenever `agentId` changes. The scroll container uses
`flex-col-reverse`, so `scrollTop = 0` corresponds to the bottom (most
recent messages).
2026-02-27 14:00:50 -05:00
edee917d88 feat: add experimental agents support (#22290)
feat: add AI chat system with agent tools and chat UI

Introduce the chatd subsystem and Agents UI for AI-powered chat
within Coder workspaces.

- Add chatd package with chat loop, message compaction, prompt
  management, and LLM provider integration (OpenAI, Anthropic)
- Add agent tools: create workspace, list/read templates, read/write/
  edit files, execute commands
- Add chat API endpoints with streaming, message editing, and
  durable reconnection
- Add database schema and migrations for chats, chat messages, chat
  providers, and chat model configs
- Add RBAC policies and dbauthz enforcement for chat resources
- Add Agents UI pages with conversation timeline, queued messages
  list, diff viewer, and model configuration panel
- Add comprehensive test coverage including coderd integration tests,
  chatd unit tests, and Storybook stories
- Gate feature behind experiments flag

---------

Co-authored-by: Cian Johnston <cian@coder.com>
Co-authored-by: Danielle Maywood <danielle@themaywoods.com>
Co-authored-by: Jeremy Ruppel <jeremy@coder.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 16:50:56 +00:00
Jake Howell bcb5b43aa7 fix: resolve entitlement check on ai bridge settings/view (#22385)
Resolves cases where the user is entitled to AI Governance but we don't
show them the page because its not enabled. If for some reason the user
doesn't have AI Bridge enabled anymore but still wants to access the old
logs page they now can.

Furthermore, we link to the docs regardless of if they have AI Bridge
enabled, this is inline with our other settings pages.
2026-02-28 03:25:04 +11:00
Jake Howell 6f3385d5e4 fix: resolve stability in scrollbar-gutter: stable (#22387)
Replaces the approach in #22061 (with a cleaner `git history`)

This now ensures that we don't attempt to cause a layout shift when the
sidebars pop-in-out of existence (when scroll locking within `radix`).
2026-02-28 03:20:43 +11:00
Paweł Banaszewski 6c097797a1 feat: add Mux icon to client column in AI Bridge request log page (#22386)
Adds Mux to the recognized clients list in AI Bridge documentation.

Adds Mux icon to AI Bridge requests log page:
<img width="1886" height="848" alt="image"
src="https://github.com/user-attachments/assets/e7cb8d47-595c-4be3-93c9-00dbea3d1153"
/>
2026-02-27 16:13:39 +00:00
Jake Howell 12372c4b1e fix: restore emptystate to <OrganizationProvisionerKeysPageView /> (#22372)
This element was receiving the provisioner key daemons and then
immediately filtering them. This lead to the default state being a table
with nothing rendered rather than the `<TableEmpty />` as we would
expect.

<img width="1133" height="608" alt="image"
src="https://github.com/user-attachments/assets/229edb00-b108-4ec3-ac2f-33633c3e5760"
/>
2026-02-28 03:11:57 +11:00
Jake Howell 0bafc05c37 feat: add permission check around <AppearanceSettingsPage /> (#22383)
This previously let auditors view the page though they can't update
anything. In a different fashion to #22382 the user will be able to see
all of this as they're logged in to the application anyway, we can
simply tell them `Sorry, no access`.
2026-02-28 02:53:29 +11:00
Jake Howell 358f521bbb feat: implement error message on failure to popup (#22380)
This change adds user-facing feedback when opening apps in a new window
fails due to popup blocking, replacing a silent no-op with a clear
recovery message. It improves reliability and supportability across
app-launch flows by helping users immediately understand and fix the
issue.
2026-02-28 02:41:44 +11:00
Jake Howell 39093dbd61 feat: implement invalidateQueries instead of location.reload() (#22377)
This was a poor UX decision to have to reload the entire page when a
template got invalidated. Simply now we refetch the data so that things
come across way smooother.
2026-02-28 02:02:07 +11:00
Jake Howell 7a3a228377 feat: implement toast on failure to export template (#22376)
This pull-request adds a small toast (fixing a previous `TODO`) when we
aren't able to export a template as we'd hoped.
2026-02-28 01:57:25 +11:00
Jeremy RuppelandClaude Sonnet 4.6 dea451de41 fix(site): await dialog close after publish to prevent act() warnings (#22334)
State updates from setIsPublishingDialogOpen,
setLastSuccessfulPublishedVersion, and navigation were firing after
waitFor resolved, causing sporadic act() warnings and timeouts in the
publish template version tests (or so says Claude Sonnet 4.6).

Fixes coder/internal#1369

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-27 08:59:22 -05:00
Jake Howell 173299fcec fix: restore dividing line in <BuildParametersPopover /> (#22373)
Related to #22367 

It was pointed out to me that we actually did regress this mildly by
removing a dividing line in the changes made in #22367, I've restored
this in a better way by taking advantage of `divide-y` and wrapping this
in a proper `<div />`.

<img width="332" height="385" alt="image"
src="https://github.com/user-attachments/assets/2827a9ae-7b54-4c48-aae9-2f6e965e7f8b"
/>
2026-02-28 00:52:55 +11:00
Jake Howell 6364cfa360 fix: resolve double border on <WorkspaceBuildPageView /> (#22374)
Not sure how I didn't see this before in #22362 , theres a sneaky double
border which makes things slightly thicker than we would expect. This
border should only be showing with the `border-b` and we forget to reset
down to `border-0` first.

<img width="1253" height="109" alt="image"
src="https://github.com/user-attachments/assets/4b31f5bd-e48b-48d8-a0a3-abeac3d6720b"
/>
<img width="1244" height="199" alt="image"
src="https://github.com/user-attachments/assets/01c2567e-e723-47ea-a5cc-ed8e025df5d0"
/>
2026-02-27 13:51:28 +00:00
Jeremy Ruppel e161083053 fix(site): use cross-browser compatible assertions in MonacoEditor story (#22337)
Switch to asserting only on the onChange spy, which is the actual
component contract being tested. Monaco's textarea value is always empty
regardless of model content, so the toHaveValue assertions were
unreliable anyway.

Fixes the new storybook test introduced in #22202
2026-02-27 08:40:23 -05:00
Jake Howell a51eb40dca fix: marshal convertLicenses() into a [] instead of nil (#22366)
This was a bad smell that was being addressed by the frontend. This type
was generating out to be a `nil`/`null` instead of an empty `License[]`.
Now this returns as an empty array and we can actively check if we have
no licenses with a length of `0`.
2026-02-28 00:23:41 +11:00
Jake Howell d204e6fb84 feat: implement file icons in <TemplateFiles /> (#22369)
This pull-request takes our icons shown in the sidebar tree and shows
them alongside the names of the files in the `Source Code` page of our
templates.

Also does a quick de-mui of this page.

<img width="637" height="345" alt="image"
src="https://github.com/user-attachments/assets/f3013eb6-9572-4d05-a683-10bb99b4e802"
/>
2026-02-27 22:53:09 +11:00
Jake Howell c6638f5547 feat: remove mui from <AppearanceSettingsPage /> (#22368)
This migrates the content from using `MUI` to the new standard `shadcn`
components on the `<AppearanceSettingsPage />`. Things should work
exactly how they did before, but with a new shinier coat of paint.

| Old | New |
| --- | --- |
| <img width="1019" height="651" alt="APPEARANCE_SETTINGS_OLD"
src="https://github.com/user-attachments/assets/d514ea17-f669-4343-99c1-9c8896ae85d8"
/> | <img width="1019" height="653" alt="APPEARANCE_SETTINGS_NEW"
src="https://github.com/user-attachments/assets/424616af-c975-43fa-bd4a-13d0b0fe136b"
/> |
2026-02-27 22:29:40 +11:00
Jake Howell fb154cb60a fix: center view raw logs button in <WorkspaceBuildPage /> (#22362)
This pull-request resolves the location of `View Raw Logs` in
`<WorkspaceBuildPage />`. It wasn't properly centred before due to some
odd-padding.

| Old | New |
| --- | --- |
| <img width="304" height="210" alt="OLD_VIEW_RAW_LOGS"
src="https://github.com/user-attachments/assets/80a5aa61-8d01-48eb-91c0-df61dd59d1fb"
/> | <img width="310" height="210" alt="NEW_VIEW_RAW_LOGS"
src="https://github.com/user-attachments/assets/8649a478-993d-4cd2-98bb-f503e0f22a5c"
/> |
2026-02-27 11:27:09 +00:00
Jake Howell 900f6ef576 fix: remove double border <BuildParametersPopover /> (#22367)
This `<Popover />` had a double border on the bottom for some reason, I
believe there used to be multiple things within this previously but
there is no longer. All this does is simply tighten things up
ever-so-slightly.

| Old | New |
| --- | --- |
| <img width="321" height="209" alt="OLD_BUILD_OPTIONS"
src="https://github.com/user-attachments/assets/9eb3322d-5ac1-4d74-ab2a-39e963eae668"
/> | <img width="321" height="209" alt="NEW_BUILD_OPTIONS"
src="https://github.com/user-attachments/assets/f2200003-0fd5-4c90-8660-5424c3cf1807"
/> |
2026-02-27 22:14:55 +11:00
Jake Howell 9cce241202 fix: migrate <TemplatePermissionsPageView /> out of mui (#22363)
This pull-request migrates `<TemplatePermissionsPageView />` out of
using MUI components. Using proper selects that are inline with the rest
of the codebase.

| Old | New |
| --- | --- |
| <img width="1030" height="284" alt="OLD_TEMPLATE_PERMISSIONS"
src="https://github.com/user-attachments/assets/778e983b-7ac1-4429-87ca-6107b176a762"
/> | <img width="1030" height="283" alt="NEW_TEMPLATE_PERMISSIONS"
src="https://github.com/user-attachments/assets/f7acf3c7-0cbd-4433-adc1-7ba7f44f3fe2"
/> |
2026-02-27 22:14:37 +11:00
Jake Howell dbc0daa64b feat: add animation to copy button <Check />s (#22319)
This pull-request adds a tiny little animation for the `<Check />` when
the copy button activates. This is inline with how `sonner` does their
animations (means things are a little more uniform).


![preview-check-animation](https://github.com/user-attachments/assets/533f644a-1d86-4b11-8ca3-c670a9913b57)
2026-02-27 21:22:57 +11:00
Jake Howell 54a7ec4b5b fix: resolve borders on <WorkspaceProxyRow /> (#22345)
This pull-request resolves some stupid border issues we were having in
`<WorkspaceProxyRow />`. We were duplicating borders and this would lead
to a `2px` border (`1px + 1px` added together). I've resolved this by
using the `divide-y` class in Tailwind so that things look more uniform.

| Old | New |
| --- | --- |
| <img width="1139" height="449" alt="OLD_WORKSPACE_PROXY"
src="https://github.com/user-attachments/assets/809926e4-ac53-4244-b215-72408ab6fa51"
/> | <img width="1139" height="449" alt="NEW_WORKSPACE_PROXY"
src="https://github.com/user-attachments/assets/b451b79c-6275-4dd5-ad2d-0ac6472b53bb"
/> |
2026-02-27 21:04:54 +11:00
Ehab Younes bf639d0016 refactor(site): use dedicated task pause/resume API endpoints (#22303)
Switch from workspace stop/start operations to the dedicated tasks pause and resume endpoints for cleaner semantics.
2026-02-26 10:46:20 +01:00
ZachandClaude Opus 4.6 4f1ddeeaad fix(site): scope TemplateSettingsPage validation error assertion to form (#22308)
The sonner migration (https://github.com/coder/coder/pull/22258) shows
validation errors in both the inline form field and a toast. Scoping the
assertion to the form element avoids flaky matches against the toast.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 13:15:03 -07:00
77006f241b fix: save empty template files (#22202)
The Monaco editor wrapper was only calling `onChange` if the template
file has content, but we want to allow saving an empty file.

Fixes #19721

Claude was used to port tests from jest to vitest, and for the stories.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Kayla はな <mckayla@hey.com>
2026-02-25 13:43:07 -05:00
Jaayden Halko 4e1cedf8fd chore: migrate workspace agent row styling to Tailwind (#22195) 2026-02-25 17:43:29 +00:00
Jake HowellandKayla はな 15a2bab1cd feat: migrate from <GlobalSnackbar /> to sonner (#22258)
Replaces our custom `<GlobalSnackbar />` (MUI Snackbar + event emitter)
with [`sonner`](https://github.com/emilkowalski/sonner). Deletes
`GlobalSnackbar/`, the custom event emitter infra, and migrates ~80
source files to `toast.success()` / `toast.error()` from `sonner`.

- ~47 error toasts now surface API error detail via
`getErrorDetail(error)` in the toast description, not just a generic
message. Coincides with #22229.
- Toast messages follow an `{Action} "{entity}" {result}.` format (e.g.
`User "alice" suspended successfully.`) since toasts persist across
navigation now.
- 17 uses of `toast.promise()` for loading → success → error lifecycle.
- Some toasts include action buttons for quick navigation (e.g. "View
task", "View template").
- Multiple toasts can stack and display simultaneously.

---------

Co-authored-by: Kayla はな <mckayla@hey.com>
2026-02-26 02:42:34 +11:00
Jake Howell 1c4d8fafc7 fix: move baseline css from mui to index.css (#22238)
This pull-request moves our baseline CSS styles from the MUI theme
(`site/src/theme/mui.ts`) definition to `index.css`. As these are global
styles they should live in one dedicated place not two.
2026-02-26 02:42:16 +11:00
Jake Howell b0b9ea6fbf fix: remove mui components from <LicenseCard /> (#22236)
This pull-request removes the `@mui/material/Paper` import from
`<LicenseCard />` so that we can nuke the `<Paper />` dependency
component. 🥳🥳🥳
2026-02-26 02:41:37 +11:00
Jake Howell 98587cfc03 fix: remove mui components from <TagInput/> (#22234)
This pull-request removes the last instance of `@mui/material/Chip` from
the codebase. And removes it from our `vite.config.mts` so we no longer
have to cache it 🙂
2026-02-26 02:41:22 +11:00
Jake Howell d2787df442 feat: add AI Bridge request logs model filter (#22230)
This pull-request implements a simple filtering logic so that we're able
to pick which model the user actually used when logs were sent to AI
Bridge.

- Add `GET /aibridge/models` API endpoint that returns distinct model
names from AI Bridge interceptions, with pagination and search support
- New `ListAIBridgeModels` SQL query using case-sensitive prefix
matching (`LIKE model || '%'`) to allow B-tree index usage
- Hand-written `ListAuthorizedAIBridgeModels` in `modelqueries.go` for
RBAC authorization filter injection
- `AIBridgeModels` search query parser in searchquery/search.go
(defaults bare terms to the `model` field)
- dbauthz wrappers, dbmetrics, and dbmock implementations for the new
query

<img width="292" height="185" alt="image"
src="https://github.com/user-attachments/assets/134771df-2d26-4c54-acc4-27f58128b351"
/>
2026-02-26 02:40:45 +11:00
Jake HowellandPhorcys 1dec1ec4ad fix: add getValidationErrorMessage() to getErrorDetail() (#22229)
This pull-request ensures that we render the validation errors back to
the user when the errors contain context as to why. Previously when we
attempted to we'd guide the user to simply checkout the Dev Console.
This wasn't a great approach as the user still would have to decode
this, the context is explicit now.

The error messages could use some improvement, but we make use of this
already in
[`Filter.tsx`](https://github.com/coder/coder/blob/main/site/src/components/Filter/Filter.tsx#L259)
so at-least its inline.

<img width="834" height="335" alt="image"
src="https://github.com/user-attachments/assets/78864d6f-b4df-4eeb-815a-3fd46cf9f31b"
/>

---------

Co-authored-by: Phorcys <57866459+phorcys420@users.noreply.github.com>
2026-02-26 02:35:25 +11:00
Jake Howell adcdbfd562 feat: implement AI Bridge client table column (#22228)
Closes #22144

Add client information column to AI Bridge request logs, showing which
coding tool initiated each request with matching icons.

- Added `Client` column to request logs table header and row, displaying
client name with icon badge
- Created `AIBridgeClientIcon` component mapping backend client
constants to their icons (Claude Code, Codex, Kilo Code, Roo Code, Zed,
Cursor, GitHub Copilot)
([ref.](https://github.com/coder/aibridge/blob/11fe0799402a652743104d047140fbeb28f02d24/bridge.go#L33-L41))
- Moved `AIBridgeModelIcon` and `AIBridgeProviderIcon` into `icons/`
subdirectory and clean up `props.className` → `className` prop
- Added new static icons: `github-copilot.svg`, `kilo-code.svg`,
`roo-code.svg` with entries in `icons.json` and `externalImages.ts`
- Sorted `externalImages.ts` map alphabetically

| Name | Preview |
| --- | --- |
| GitHub Copilot | <img width="1332" height="67" alt="image 11"
src="https://github.com/user-attachments/assets/0b06ea42-aaf9-431b-9f9f-3a0146d3eb44"
/> |
| Claude | <img width="1332" height="327" alt="PREVIEW_CLAUDE"
src="https://github.com/user-attachments/assets/7e1afcbc-b94b-4017-bbdc-f40e0ca237d8"
/> |
| Codex CLI | <img width="1332" height="67" alt="PREVIEW_CODEX"
src="https://github.com/user-attachments/assets/2a9ffde1-2516-4d81-baf0-6e689d8a37bf"
/> |
| Cursor | <img width="1332" height="67" alt="PREVIEW_CURSOR"
src="https://github.com/user-attachments/assets/2c4883e8-35cd-4b08-8463-82ba7c95d96d"
/> |
| KiloCode | <img width="1332" height="132" alt="PREVIEW_KILO_CODE"
src="https://github.com/user-attachments/assets/e8bc2854-6fdb-47e0-a304-fb138ac0e2fe"
/> |
| Roo Code | <img width="1332" height="262" alt="PREVIEW_ROO_CODE"
src="https://github.com/user-attachments/assets/d2977090-525b-44ee-9ab6-e6019e559bbd"
/> |
| Zed | <img width="1332" height="67" alt="PREVIEW_ZED"
src="https://github.com/user-attachments/assets/1d982ae0-1d08-4b85-8b4a-5c13fb7754f1"
/> |
2026-02-25 02:38:57 +11:00
Kacper Sawicki 1e274063d4 feat(coderd): filter expired API tokens server-side (#22263)
## Summary

Moves expired token filtering from client-side to server-side by adding
an `include_expired` parameter to the `GetAPIKeysByLoginType` and
`GetAPIKeysByUserID` database queries. This is more efficient for large
deployments with many expired/short-lived tokens.

## Changes

- Add `include_expired` parameter to SQL queries using `OR`
short-circuit
- Add `include_expired` query parameter to `GET
/users/{user}/keys/tokens`
- Add `IncludeExpired` field to `codersdk.TokensFilter`
- Remove client-side filtering from CLI `tokens list` command
- Add `TestTokensFilterExpired` test

Fixes coder/internal#1357
2026-02-24 15:27:03 +00:00
blinkagent[bot]andblink-so[bot] bf076fb7ee feat: add anthropic and gemini-monochrome icons (#22270)
Adds two new icons to the icon library:

- **`anthropic.svg`** — Anthropic logo
- **`gemini-monochrome.svg`** — Gemini logo, monochrome variant

Both use `monochrome` theme handling to adapt for dark and light
backgrounds.

### Changes
- Added `anthropic.svg` and `gemini-monochrome.svg` to
`site/static/icon/`
- Registered both in `site/src/theme/icons.json` (alphabetically sorted)
- Added `monochrome` theme handling for both in
`site/src/theme/externalImages.ts`

---
Created on behalf of @tracyjohnsonux

---------

Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
2026-02-24 13:15:21 +11:00
Sushant P 37a8e61ea2 chore: move Shared Workspaces from experiments to beta (#22206)
* Removed the shared-workspaces experiment and cleaned up related
middleware
* Added beta tagging to the UI for shared workspaces
2026-02-23 08:30:32 -08:00
Jake Howell 50823a28fd fix: remove mui components from <AccountUserGroups /> (#22235)
This pull-request removes the `@mui/material/Grid` import from
`<AccountUserGroups />` so that we can nuke the `<Grid />` dependency
component. 🥳🥳🥳
2026-02-23 22:29:25 +11:00
Jake Howell e9b66a8083 fix: remove mui components from <PaginationWidgetBase /> (#22237) 2026-02-23 22:25:13 +11:00
Thomas Kosiewski b776a14b46 fix(coderd): harden OAuth2 provider security (#22194)
## Summary

Harden the OAuth2 provider with multiple security fixes addressing
`coder/security#121` (CSRF session takeover) and converge on OAuth 2.1
compliance.

### Security Fixes

| Fix | Description | Commits |
|-----|-------------|---------|
| **CSRF on `/oauth2/authorize`** | Enforce CSRF protection on the
authorize endpoint POST (consent form submission) | `ba7d646`, `b94a64e`
|
| **Clickjacking: `frame-ancestors` CSP** | Prevent consent page from
being iframed (`Content-Security-Policy: frame-ancestors 'none'` +
`X-Frame-Options: DENY`) | `597aeb2` |
| **Exact redirect URI matching** | Changed from prefix matching to full
string exact matching per OAuth 2.1 §4.1.2.1 | `73d64b1`, `93897f1` |
| **Store & verify `redirect_uri`** | Store redirect_uri with auth code
in DB, verify at token exchange matches exactly (RFC 6749 §4.1.3) |
`50569b9`, `d7ca315` |
| **Mandatory PKCE** | Require `code_challenge` at authorization (for
`response_type=code`) + unconditional `code_verifier` verification at
token exchange | `d7ca315`, `1cda1a9` |
| **Reject implicit grant** | `response_type=token` now returns
`unsupported_response_type` error page (OAuth 2.1 removes implicit flow)
| `d7ca315`, `91b8863` |

### Changes by File

**`coderd/httpmw/csrf.go`** — Extended the CSRF `ExemptFunc` to enforce
CSRF on `/oauth2/authorize` in addition to `/api` routes. The consent
form POST is now CSRF-protected to prevent cross-site authorization code
theft.

**`site/site.go`** — Added `Content-Security-Policy: frame-ancestors
'none'` and `X-Frame-Options: DENY` headers to `RenderOAuthAllowPage`
(consent page only — does not affect the SPA/global CSP used by AI
tasks).

**`coderd/httpapi/queryparams.go`** — Changed `RedirectURL` from prefix
matching (`strings.HasPrefix(v.Path, base.Path)`) to full URI exact
matching (`v.String() != base.String()`), comparing scheme, host, path,
and query.

**`coderd/oauth2provider/authorize.go`** — Added PKCE enforcement:
`code_challenge` is required when `response_type=code` (via a
conditional check, not `RequiredNotEmpty`, so `response_type=token` can
reach the explicit rejection path). `ShowAuthorizePage` (GET) validates
`response_type` before rendering and returns a 400 error page for
unsupported types. `ProcessAuthorize` (POST) stores the `redirect_uri`
with the auth code when explicitly provided.

**`coderd/oauth2provider/tokens.go`** — PKCE verification is now
unconditional (not gated on `code_challenge` being present in DB). If
the stored code has a `redirect_uri`, the token endpoint verifies it
matches exactly — mismatch returns `errBadCode` → `invalid_grant`.
Missing `code_verifier` returns `invalid_grant`.

**`codersdk/oauth2.go`** — `OAuth2ProviderResponseTypeToken` constant
and `Valid()` acceptance are **kept** so the authorize handler can parse
`response_type=token` and return the proper `unsupported_response_type`
error rather than failing at parameter validation.

**`coderd/database/migrations/000421_*`** — Added `redirect_uri text`
column to `oauth2_provider_app_codes`.

### Design Decisions

**`state` parameter remains optional** — The plan initially required
`state` via `RequiredNotEmpty`, but this was reverted in `376a753` to
avoid breaking existing clients. The `state` is still hashed and stored
when provided (via `state_hash` column), securing clients that opt in.

**`response_type=token` kept in `Valid()`** — Removing it from `Valid()`
would cause the parameter parser to reject the request before the
authorize handler can return the proper `unsupported_response_type`
error. The constant is kept for correct error handling flow.

**CSP scoped to consent page only** — `frame-ancestors 'none'` is set
only on the OAuth consent page renderer, not globally. The SPA/global
CSP was previously changed to allow framing for AI tasks
([#18102](https://github.com/coder/coder/pull/18102)); this change does
not regress that.

### Out of Scope (follow-up PRs)

- Bearer tokens in query strings (needs internal caller audit)
- Scope enforcement on OAuth2 tokens
- Rate limiting on dynamic client registration


---

<details>
<summary>📋 Implementation Plan</summary>

# Plan: Harden OAuth2 Provider — Security Fixes + OAuth 2.1 Compliance

## Context & Why

Security issue `coder/security#121` reports a critical session takeover
via CSRF on the OAuth2 provider. This plan covers all remaining security
fixes from that issue **plus** convergence on OAuth 2.1 requirements.
The goal is a single PR that closes all actionable gaps.

## Current State (already committed on branch `csrf-sjx1`)

| Fix | Status | Commits |
|-----|--------|---------|
| Fix 1: CSRF on `/oauth2/authorize` | ✅ Done | `ba7d646`, `b94a64e` |
| CSRF token in consent form HTML | ✅ Done | `b94a64e` |
| `state_hash` column + storage | ✅ Done (hash stored, but state still
optional) | `9167d83`, `b94a64e` |
| Tests for CSRF + state hash | ✅ Done | `e4119b5` |

## Remaining Work

### ~~Fix 2 — Require `state` parameter~~ (DROPPED)

> **Decision:** Do not enforce `state` as required. The `state`
parameter is still hashed and stored when provided (via
`hashOAuth2State` / `state_hash` column from prior commits), but clients
are not forced to supply it. This avoids breaking existing integrations
that omit state.

**Rollback:** Remove `"state"` from the `RequiredNotEmpty` call in
`coderd/oauth2provider/authorize.go:42`:

```go
// BEFORE (current on branch)
p.RequiredNotEmpty("response_type", "client_id", "state", "code_challenge")

// AFTER
p.RequiredNotEmpty("response_type", "client_id", "code_challenge")
```

No test changes needed — tests already pass `state` voluntarily.

### Fix 4 — Exact redirect URI matching

Currently `coderd/httpapi/queryparams.go:233` uses prefix matching:

```go
// CURRENT — prefix match
if v.Host != base.Host || !strings.HasPrefix(v.Path, base.Path) {
```

OAuth 2.1 requires **exact string matching**. Change to:

```go
// AFTER — exact match (OAuth 2.1 §4.1.2.1)
if v.Host != base.Host || v.Path != base.Path {
```

**File: `coderd/httpapi/queryparams.go` — `RedirectURL` method**

Also update the error message from "must be a subset of" to "must
exactly match".

**Additionally**, store `redirect_uri` with the auth code and verify at
the token endpoint (RFC 6749 §4.1.3):

1. **New migration** (same migration file or a new `000421`): Add
`redirect_uri text` column to `oauth2_provider_app_codes`
2. **Update INSERT query** in `coderd/database/queries/oauth2.sql` to
include `redirect_uri`
3. **`coderd/oauth2provider/authorize.go`**: Store
`params.redirectURL.String()` when inserting the code
4. **`coderd/oauth2provider/tokens.go`**: After retrieving the code from
DB, verify that `redirect_uri` from the token request matches the stored
value exactly. Currently `tokens.go:103` calls `p.RedirectURL(vals,
callbackURL, "redirect_uri")` for prefix validation only — it must
compare against the stored redirect_uri from the code, not just the
app's callback URL.

<details>
<summary>Why both exact match AND store+verify?</summary>

Exact matching at the authorize endpoint prevents open redirectors
(attacker can't use a sub-path).
Storing and verifying at the token endpoint prevents code injection — an
attacker who steals a code can't exchange it with a different
redirect_uri than was originally authorized. This is required by RFC
6749 §4.1.3 and OAuth 2.1.
</details>

### Fix 7 — `frame-ancestors` CSP on consent page

The consent page can be iframed by a workspace app (same-site), which is
the attack vector. Add a `Content-Security-Policy` header to prevent
framing.

**File: `site/site.go` — `RenderOAuthAllowPage` function (~line 731)**

Before writing the response, add:

```go
func RenderOAuthAllowPage(rw http.ResponseWriter, r *http.Request, data RenderOAuthAllowData) {
    rw.Header().Set("Content-Type", "text/html; charset=utf-8")
    // Prevent the consent page from being framed to mitigate
    // clickjacking attacks (coder/security#121).
    rw.Header().Set("Content-Security-Policy", "frame-ancestors 'none'")
    rw.Header().Set("X-Frame-Options", "DENY")
    ...
```

Both headers for defense-in-depth (CSP for modern browsers,
X-Frame-Options for legacy).

### OAuth 2.1 — Mandatory PKCE

Currently PKCE is checked only when `code_challenge` was provided during
authorization (`tokens.go:258`):

```go
// CURRENT — conditional check
if dbCode.CodeChallenge.Valid && dbCode.CodeChallenge.String != "" {
    // verify PKCE
}
```

OAuth 2.1 requires PKCE for ALL authorization code flows. Change to:

**File: `coderd/oauth2provider/authorize.go`** — Add `"code_challenge"`
to required params:

```go
p.RequiredNotEmpty("response_type", "client_id", "code_challenge")
```

**File: `coderd/oauth2provider/tokens.go:257-265`** — Make PKCE
verification unconditional:

```go
// AFTER — PKCE always required (OAuth 2.1)
if req.CodeVerifier == "" {
    return codersdk.OAuth2TokenResponse{}, errInvalidPKCE
}
if !dbCode.CodeChallenge.Valid || dbCode.CodeChallenge.String == "" {
    // Code was issued without a challenge — should not happen
    // with the authorize endpoint enforcement, but defend in
    // depth.
    return codersdk.OAuth2TokenResponse{}, errInvalidPKCE
}
if !VerifyPKCE(dbCode.CodeChallenge.String, req.CodeVerifier) {
    return codersdk.OAuth2TokenResponse{}, errInvalidPKCE
}
```

**File: `codersdk/oauth2.go`** — Remove
`OAuth2ProviderResponseTypeToken` from the enum or reject it explicitly
in the authorize handler. Currently it's defined at line 216 but the
handler ignores `response_type` and always issues a code. We should
either:
- (a) Remove the `"token"` variant from the enum and reject it with
`unsupported_response_type`, OR
- (b) Add an explicit check in `ProcessAuthorize` that rejects
`response_type=token`

Option (b) is simpler and more backwards-compatible:

```go
// In ProcessAuthorize, after extracting params:
if params.responseType != codersdk.OAuth2ProviderResponseTypeCode {
    httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest,
        codersdk.OAuth2ErrorCodeUnsupportedResponseType,
        "Only response_type=code is supported")
    return
}
```

### OAuth 2.1 — Bearer tokens in query strings

`coderd/httpmw/apikey.go:743` accepts `access_token` from URL query
parameters. OAuth 2.1 prohibits this. However, this may be used
internally (e.g., workspace apps, DERP). Need to audit callers before
removing.

**Approach:** This is a larger change with potential breakage. Mark as a
**separate follow-up issue** rather than including in this PR. Document
the finding.

### OAuth 2.1 — Removed flows

✅ **Already compliant.** `tokens.go` only supports `authorization_code`
and `refresh_token` grant types. The implicit grant
(`response_type=token`) will be explicitly rejected per the PKCE section
above.

### OAuth 2.1 — Refresh token rotation

✅ **Already compliant.** `tokens.go:442` deletes the old API key when a
refresh token is used.

## Migration Plan

All DB changes can go in a single new migration (or extend 000420 if the
branch is rebased before merge). Columns to add:
- `redirect_uri text` on `oauth2_provider_app_codes`

The `state_hash` column is already added by migration 000420.

## Implementation Order

1. **Fix 7** — CSP headers on consent page (isolated, no deps)
2. ~~**Fix 2** — Require `state` parameter~~ (DROPPED — state stays
optional)
3. **Fix 4** — Exact redirect URI matching + store/verify redirect_uri
4. **PKCE mandatory** — Require `code_challenge` + reject
`response_type=token`
5. **Rollback** — Remove `"state"` from `RequiredNotEmpty` in
`authorize.go`
6. **Tests** — Update/add tests for all changes
7. **`make gen`** after DB changes

## Out of Scope (separate PRs)

- Bearer tokens in query strings (needs internal caller audit)
- Scope enforcement on OAuth2 tokens
- Rate limiting / quota on dynamic client registration

</details>

---
_Generated with [`mux`](https://github.com/coder/mux) • Model:
`anthropic:claude-opus-4-6` • Thinking: `xhigh`_
2026-02-23 12:18:44 +01:00
Jake Howell 7825c02876 fix: remove all deprecated @mui/ alert classes (#22233)
This pull-request removes all the magic of `@mui/material/Alert` 🥳 We're
officially free of any alerts that are being handled by Material UI so
this is dead code.
2026-02-23 12:26:41 +11:00
Jake Howell 64e0bfa880 feat: animate between <TabLink />s (#22113) 2026-02-21 10:01:53 +11:00