Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
</details>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Splits the single `coder` artifact (containing all platforms in a 1.3GB
zip) into individual artifacts per OS/arch/format.
## Problem
All CI build artifacts are uploaded as a single artifact named `coder`,
producing a 1.3GB zip containing every platform's binary. This makes it
impossible to download a single platform's binary without pulling the
entire bundle.
## Solution
Upload each platform/format combination as a separate artifact:
| Artifact Name | Contents |
|---|---|
| `coder-linux-amd64.tar.gz` | Linux amd64 tarball |
| `coder-linux-amd64.deb` | Linux amd64 deb package |
| `coder-linux-arm64.tar.gz` | Linux arm64 tarball |
| `coder-linux-arm64.deb` | Linux arm64 deb package |
| `coder-linux-armv7.tar.gz` | Linux armv7 tarball |
| `coder-linux-armv7.deb` | Linux armv7 deb package |
| `coder-windows-amd64.zip` | Windows amd64 zip |
## Plan
This is the first step toward letting customers install directly from
`main` via:
```bash
curl -L https://coder.com/install.sh | sh -s -- --unsafe-unstable
```
GitHub Actions artifact downloads require authentication even for public
repos, so the next steps are to add a small Cloudflare Worker (similar
to the one we already have for `install.sh`) that:
1. Lists artifacts via the GitHub API (unauthenticated) to find the
latest artifact ID for the requested platform
2. Calls the download endpoint with a GitHub token (CF Worker secret) to
get a 302 redirect to a time-limited Azure Blob URL
3. Redirects the caller to that URL (which requires no auth)
This gives us publicly accessible per-platform URLs that the
`--unsafe-unstable` flag would point at. The worker doesn't proxy the
binary itself — it only proxies the metadata API call (~1KB) and
redirects for the actual download.
This PR splits the artifacts so the worker can serve individual platform
downloads (~200MB each) instead of forcing a 1.3GB bundle.
ForkReap's syscall.ForkExec and process-directed signals remain
flaky in CI despite the subprocess isolation added in #22894.
Restore the testutil.InCI() skip guard that was removed in that
change.
Fixescoder/internal#1402
## Summary
Refactors the Git panel in the Agents page to consolidate duplicated
diff viewer code and significantly improve the UI.
### Deduplication
- **RemoteDiffPanel** now uses the shared `DiffViewer` component instead
of duplicating file tree, lazy loading, scroll tracking, and layout
(~500 lines removed).
- Renamed `RepoChangesPanel` → `LocalDiffPanel`, `FilesChangedPanel` →
`RemoteDiffPanel` to reflect actual scope.
- Removed `headerLeft`/`headerRight` abstraction from `DiffViewer` —
each consumer owns its own header.
- Replaced hand-rolled `ChatDiffStatusResponse` with auto-generated
`ChatDiffStatus` from `typesGenerated.ts`.
### Tab Redesign
- Per-repo tabs: each local repo gets its own tab (`Working <repo>`)
instead of a single stacked view.
- PR tab shows state icon + PR title; branch-only tab shows branch icon.
- Tabs use `Button variant="outline"` matching the Git/Desktop tab
style.
- Radix `ScrollArea` with thin horizontal scrollbar for tab overflow.
- Diff style toggle and refresh button lifted to shared toolbar, always
visible.
### PR Header
- Compact sub-header: `base_branch ←`, state badge
(`Open`/`Draft`/`Merged`/`Closed`), diff stats, and `View PR` button.
- GitHub-style state-aware icons (green open, gray draft, purple merged,
red closed).
- New API fields synced: `base_branch`, `author_login`, `pr_number`,
`commits`, `approved`, `reviewer_count`.
### Local Changes Header
- Compact sub-header: branch name, repo root path, diff stats, and
`Commit` button (styled to match `View PR`).
- `CircleDotIcon` (amber) for working changes tabs — universal
"modified" indicator.
### Visual Polish
- All text in sub-headers and buttons at 13px matching chat font size.
- All badges (`DiffStatBadge`, PR state, `View PR`, `Commit`) use
consistent `border-border-default`, `rounded-sm`, `leading-5`.
- No background color on diff viewer header bars.
- Tabs hidden when their view has no content; auto-switch when active
tab disappears.
### Stories
- New `GitPanel.stories.tsx` covering: open PR + working changes, draft
PR, merged PR, closed PR, branch only, working changes only, multiple
repos, empty state.
- Removed old `LocalDiffPanel.stories.tsx` and
`RemoteDiffPanel.stories.tsx`.
## Problem
Sending a message on the `/agents` page triggers a burst of redundant
HTTP requests. The root cause is that chat mutations call
`invalidateQueries({ queryKey: ["chats"] })` which, due to React Query's
default **prefix matching**, cascades to every query whose key starts
with `["chats"]`:
- `["chats", {archived: false}]` — infinite sidebar list
- `["chats", chatId]` — individual chat detail
- `["chats", chatId, "messages"]` — all messages
- `["chats", chatId, "diff-status"]` — diff status
- `["chats", chatId, "diff-contents"]` — diff contents
- `["chats", "costSummary", ...]` — cost summaries
All of these have active subscribers on the page, so each one fires a
network request. The WebSocket stream already delivers these updates in
real-time, making the HTTP refetches completely redundant.
## Fix
| Mutation | Before | After |
|---|---|---|
| `createChatMessage` | `invalidateQueries({ queryKey: chatsKey })` —
prefix cascade | **Removed** — WebSocket delivers messages + sidebar
updates |
| `interruptChat` | `invalidateQueries({ queryKey: chatsKey })` — prefix
cascade | **Removed** — WebSocket delivers status changes |
| `editChatMessage` | 3 broad invalidations including `chatsKey` prefix
| 2 targeted with `exact: true`: `chatKey(id)` + `chatMessagesKey(id)` |
| `promoteChatQueuedMessage` | 3 broad invalidations including
`chatsKey` prefix | 2 targeted with `exact: true`: `chatKey(id)` +
`chatMessagesKey(id)` |
`editChatMessage` keeps `chatMessagesKey` invalidation because editing
truncates messages server-side and the WebSocket can only insert/update,
never remove stale entries.
## Net effect
Sending a message previously triggered **5–7 HTTP requests**. Now it
triggers **zero** — the WebSocket handles everything.
## Tests
Added `describe("mutation invalidation scope")` with 8 test cases
asserting that each mutation only invalidates the queries it genuinely
needs.
## Problem
When creating a new chat in the agents page (`/agents`), the chat could
appear multiple times in the sidebar. This was a race condition
triggered by the WebSocket `created` event handler.
## Root Cause
`updateInfiniteChatsCache` applies its updater function **independently
on each page** of the infinite query:
```ts
const nextPages = prev.pages.map((page) => updater(page));
```
When the `watchChats` WebSocket received a `"created"` event, the
handler checked `exists` only within the *current page*, then prepended
the new chat if not found:
```ts
updateInfiniteChatsCache(queryClient, (chats) => {
const exists = chats.some((c) => c.id === updatedChat.id);
// ...
if (chatEvent.kind === "created") {
return [updatedChat, ...chats]; // runs per page!
}
});
```
Since a brand-new chat doesn't exist in any page, **every loaded page**
prepends it. After `pages.flat()`, the chat appears once per loaded page
in the sidebar.
## Fix
- Added `prependToInfiniteChatsCache` in `chats.ts` that checks across
**all pages** before prepending, and only adds to page 0.
- Split the WebSocket handler so `"created"` events use the new safe
prepend, while update events (`title_change`, `status_change`) continue
using `updateInfiniteChatsCache` (which is safe for `.map()` operations
that don't add entries).
Adds the `head_branch` field (the source/feature branch name of a PR) to
the diff status pipeline. Previously only `base_branch` (target branch)
and the head commit SHA were captured from the GitHub API, but not the
head branch name itself.
## Changes
- **Migration 438**: Add `head_branch` nullable TEXT column to
`chat_diff_statuses`
- **gitprovider**: Parse `head.ref` from the GitHub API response
(alongside `head.sha`) and add `HeadBranch` to `PRStatus`
- **gitsync**: Wire `HeadBranch` through `refreshOne()` into the DB
upsert params
- **worker**: Map `HeadBranch` in `chatDiffStatusFromRow()`
- **coderd**: Convert `HeadBranch` in `convertChatDiffStatus()`
- **codersdk**: Expose as `head_branch` (`*string`, omitempty) in
`ChatDiffStatus` API response
- **Tests**: Updated `github_test.go` pull JSON fixtures and assertions
### Motivation
- The desktop watch handler opened a VNC stream using the chat's
workspace ID while only relying on workspace read permissions, allowing
read-only users to escalate to interactive desktop access.
- Enforce connect-level authorization so only actors with
`ActionApplicationConnect` or `ActionSSH` can open the desktop stream.
### Description
- Added an explicit workspace lookup in `watchChatDesktop` using
`GetWorkspaceByID` to obtain a workspace object for authorization.
- Require the requester to be authorized for either
`policy.ActionApplicationConnect` or `policy.ActionSSH` on the workspace
before proceeding to locate agents or connect to the VNC stream, and
return `403 Forbidden` when neither permission is present.
- The change is minimal and localized to `coderd/chats.go` and does not
alter other code paths or behavior when the requester has the necessary
connect permissions.
### Testing
- Ran `gofmt -w coderd/chats.go` to format the modified file, which
succeeded.
- Attempted to run the unit test `TestWatchChatDesktop/NoWorkspace` via
`go test` in this environment but the test run did not complete within
the environment constraints and did not produce a full pass result.
- Attempted to run the repository pre-commit/gen steps but they could
not complete due to missing developer tooling and services in this
environment (e.g. `sqlc`, `mockgen`, `protoc` plugins and test services
like Docker/Postgres), so full pre-commit validation did not finish
here.
- Code review and static validation confirm the added authorization
check properly prevents read-only access from opening the desktop VNC
stream.
------
[Codex
Task](https://chatgpt.com/codex/tasks/task_b_69b46a4ac5c4832ea9d330aeba43c32d)
## Problem
The agents sidebar infinite scroll was spamming the `/api/v2/chats`
endpoint with duplicate requests at the same offset, caused by the
`LoadMoreSentinel` component.
### Root cause
`onLoadMore` is an inline arrow function (`() => void
chatsQuery.fetchNextPage()`), creating a **new function reference on
every render**. The `useEffect` in `LoadMoreSentinel` depended on
`[onLoadMore]`, so it tore down and re-created the
`IntersectionObserver` on every render. Each new observer immediately
fired its callback when the sentinel was already visible, triggering
duplicate fetches.
## Fix
- Store `onLoadMore` and `isFetchingNextPage` in **refs** so the
observer callback always reads the latest values without needing to tear
down/re-create.
- Create the `IntersectionObserver` **once on mount** (empty deps
array).
- **Guard** against calling `onLoadMore` while `isFetchingNextPage` is
true.
## Tests
- **LoadMoreSentinel behavior tests** (6 tests): verifies no duplicate
calls across re-renders, proper `isFetchingNextPage` gating, ref-based
observer stability, and correct resume after fetch completes.
- **`infiniteChats` query factory tests** (6 tests): covers
`getNextPageParam` and `queryFn` offset computation to prevent
pagination regressions.
Surfaces cache token data in the analytics views and fixes table
spacing.
### Changes
- **Cache token columns**: Added cache read and cache write token counts
to all analytics views (user and admin), from SQL queries through Go SDK
types to the frontend tables and summary cards.
- **Table spacing fix**: Replaced the bare React fragment in
`ChatCostSummaryView` with a `space-y-6` container so the model and chat
breakdown tables no longer overlap.
### Data flow
`chat_messages` table already stores `cache_read_tokens` and
`cache_creation_tokens` (and uses them for cost calculation). This PR
aggregates and displays them alongside input/output tokens in:
- Summary cards (6 cards: Total Cost, Input, Output, Cache Read, Cache
Write, Messages)
- Per-model breakdown table
- Per-chat breakdown table
- Admin per-user table
This PR adds a `WatchAllWorkspaces` function with `watch-all-workspaces`
endpoint, which can be used to listen on a single global pubsub channel
for _all_ workspace build updates, and makes use of it in the autostart
scaletest.
This negates the need to use a workspace watch pubsub channel _per_
workspace, which has auth overhead associated with each call. This is
especially relevant in situations such as the autostart scaletest, where
we need to start/stop a set of workspaces before we can configure their
autostart config. The overhead associated with all the watch requests
skews the scaletest results and makes it harder to reason about the
performance of the autostart feature itself.
The autostart scaletest also no longer generates its own metrics nor
does it wait for all the workspaces to actually start via autostart. We
should update the scaletest dashboard after both PRs are merged to
measure autostart performance via the new metrics.
The new function/endpoint and its usage in the autostart scaletest are
gated behind an experiment feature flag, this is something we should
discuss whether we want to enable the endpoint in prod by default or
not. If so, we can remove the experiment.
---------
Signed-off-by: Callum Styan <callumstyan@gmail.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Callum Styan <callum@coder.com>
Install Google Chrome stable directly from `dl.google.com`. Ubuntu 22.04
ships `chromium-browser` as a snap-only package, which does not work in
Docker containers.
```dockerfile
RUN wget -q https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && \\
apt-get install --yes ./google-chrome-stable_current_amd64.deb && \\
rm google-chrome-stable_current_amd64.deb
```
Verified in a running dogfood workspace:
```
$ google-chrome --version
Google Chrome 146.0.7680.75
```
- Use t.Errorf in chattest non-streaming helpers so encoding
failures fail the test
- Thread testing.TB into writeResponsesAPIStreaming and log
SSE write errors instead of silently dropping them
- Bump createworkspace DB error log from Warn to Error
- Use errors.Join for timeout + output error in execute.go
Add UI components for viewing and managing LLM chat cost analytics.
## Changes
- `UserAnalyticsDialog`: personal cost summary with 30-day date range
- `ChatCostSummaryView`: shared component for cost breakdowns by model
and chat
- `ConfigureAgentsDialog`: admin Usage tab with deployment-wide cost
rollup
- Storybook stories for all new and existing components
- Replace `ModelsSection.test.tsx`, `DashboardLayout.test.tsx`,
`AuditPage.test.tsx` with Storybook stories
- Cost-related API client methods and React Query hooks
- Analytics utilities for formatting microdollar values
Backend: #23036
Implement the backend for the desktop feature for agents.
- Adds a new `/api/experimental/chats/$id/desktop` endpoint to coderd
which exposes a VNC stream from a
[portabledesktop](https://github.com/coder/portabledesktop) process
running inside the workspace
- Adds a new `spawn_computer_use_agent` tool to chatd, which spawns a
subagent that has access to the `computer` tool which lets it interact
with the `portabledesktop` process running inside the workspace
- Adds the plumbing to make the above possible
There's a follow up frontend PR here:
https://github.com/coder/coder/pull/23006
## Problem
The `/agents` page has a voice input feature that uses the Web Speech
API (`webkitSpeechRecognition`), but clicking the mic button always
results in a `"not-allowed"` error — even when the browser has
microphone permission granted.
## Root Cause
The `secureHeaders()` function in `site/site.go` sets a
`Permissions-Policy` header with `microphone=()`, which completely
disables microphone access for the page at the HTTP level. This
overrides any browser-level mic permission grants and causes the Web
Speech API to immediately fire an `onerror` event with `error:
"not-allowed"`.
## Fix
Change `microphone=()` to `microphone=(self)`, which:
- Allows the Coder origin itself to use the microphone (enabling the Web
Speech API voice input)
- Still blocks cross-origin iframes from accessing the microphone
This is the minimal permission change needed — `(self)` is more
restrictive than removing the policy entirely, maintaining the security
intent of the original header.
## Testing
1. Navigate to `/agents`
2. Click the mic button in the chat input
3. Verify voice input works (browser will prompt for mic permission if
not already granted)
4. Verify `Permissions-Policy` response header now shows
`microphone=(self)` instead of `microphone=()`
---------
Co-authored-by: blink-so[bot] <211532188+blink-so[bot]@users.noreply.github.com>
Handle previously ignored error return values in coderd:
- coderd/chats.go: check sendEvent errors, log on failure
- coderd/chatd/chattest: thread testing.TB through server structs,
replace log.Printf with t.Logf, check writeSSEEvent errors
- coderd/chatd/chattool/createworkspace.go: log UpdateChatWorkspace
failure instead of discarding both return values
- coderd/chatd/chattool/execute.go: surface ProcessOutput error in
the timeout message returned to the caller
- coderd/provisionerdserver: log stream.Send failure in the
DownloadFile error helper
- Adds `extractJSON()` to strip markdown code fences before JSON parsing and wire into the `json.Unmarshal` call in `generateFromAnthropic`.
- Accepts variadic `RequestOption` in `generateFromAnthropic` so tests can inject a mock Anthropic server via `WithBaseURL`.
- Adds table-driven cases covering bare JSON, fenced with/without language tag, surrounding whitespace, and multiline JSON.
- Adds end-to-end cases using `httptest.NewServer` to serve fake Anthropic SSE streams with bare and fenced responses.
Tests that call ForkReap or send signals to their own process now
re-exec as isolated subprocesses. This prevents ForkReap's
syscall.ForkExec and process-directed signals from interfering
with the parent test binary or other tests running in parallel.
Also:
- Wait for the reaper goroutine to fully exit between subtests
to prevent overlapping reapers from competing on Wait4(-1).
- Register signal handlers synchronously before spawning the
forwarding goroutine so no signal is lost between ForkExec
and the handler being ready.
Add cost tracking for LLM chat interactions with microdollar precision.
## Changes
- Add `chatcost` package for per-message cost calculation using
`shopspring/decimal` for intermediate arithmetic
- **Ceil rounding policy**: fractional micros round UP to next whole
micro (applied once after summing all components)
- Database migration: `total_cost_micros` BIGINT column with historical
backfill and `created_at` index
- API endpoints: per-user cost summary and admin rollup under
`/api/experimental/chats/cost/`
- SDK types: `ChatCostSummary`, `ChatCostModelBreakdown`,
`ChatCostUserRollup`
- Fix `modeloptionsgen` to handle `decimal.Decimal` as opaque numeric
type
- Update frontend pricing test fixtures for string decimal types
## Design decisions
- `NULL` = unpriced (no matching model config), `0` = free
- Reasoning tokens included in output tokens (no double-counting)
- Integer microdollars (BIGINT) for storage and API responses
- Price config uses `decimal.Decimal` for exact parsing; totals use
`int64`
Frontend: #23037
## Problem
Clicking the microphone button on `/agents` briefly activates recording
then immediately stops, focusing the text input.
## Root causes
**1. Race condition in `start()`** — When aborting a previous
recognition instance, the old instance's `onend` fires
**asynchronously** in real browsers (unlike our mock which fires it
synchronously). The stale `onend` callback then sets `isRecording=false`
and nullifies `recognitionRef.current`, killing the new recording
session. The unit tests didn't catch this because the mock fires `onend`
synchronously inside `abort()`.
**2. Silent `onerror` handler** — The `onerror` callback completely
discarded the error event. If the browser denied mic permission or the
speech service was unreachable (Chrome sends audio to Google servers),
recording silently died with no feedback.
**3. No cleanup on unmount** — The hook leaked a running recognition
instance if the component unmounted while recording.
## Fixes
- Guard `onend`/`onerror` callbacks with `recognitionRef.current !==
recognition` so stale instances are ignored
- Expose `error: string | null` state from the hook; surface it in the
UI ("Mic access denied" / "Voice input failed")
- Add a cleanup `useEffect` that aborts recognition on unmount
- Added 4 new tests covering the race condition, error exposure, and
error clearing
related to: https://github.com/coder/internal/issues/1387
Sometimes our tests using DERP flake like the above, but we have no information at the DERP layer about why. This enables verbose DERP logging in CI.
Logs are kinda spammy, but most tests do not force DERP and so only use it for discovery (disco).
A test like TestWorkspaceAgentListeningPorts/OK_BlockDirect drops around 50 DERP packets e.g.
```
t.go:111: 2026-03-13 15:20:52.201 [debu] agent.net.tailnet.net.wgengine: magicsock: got derp-999 packet: "\x04\x00\x00\x00C}\xe0\xb9\x00\x00\x00\x00\x00\x00\x00\x00}\xae;l\xd9Hk8\xa8l\x1cK\xedO{狦)\x18NIw\xc4k\xd2-\x19\xbf\xfb\xdd\x17\xa9b\xac\xfd#\xf7\xcaC\xbe\vq(u=\xa7\x16\xe9\x9aLjS\x1fXL\x19y\xf4\x1dE%\xb3\xff\x9d:8\xa9\x95X\xfe\xf8\x95\x7f\x9dv$\f\xf4\xbe"
t.go:111: 2026-03-13 15:20:52.201 [debu] agent.net.tailnet.net.wgengine: magicsock: got derp-999 packet: "\x04\x00\x00\x00C}\xe0\xb9\x01\x00\x00\x00\x00\x00\x00\x00\x05x\xb6RD;\xb4\x80\xb6\x0f\xf6KŠc\xfb1\xbd\x06\xb70K3\x97`\x8d\xd2\x14\xed\xc5\xd6\xc6\xcaV\xbf\x878\xb2Ƥ\xf0\xd5\xf7\xc0\x1b\x9f\x04Y\x03\x17\xd4\x06\xee\xb2G\r){\x9f\xde\xe0(\xb5N\xfejR_\xf6q\xa4\xfaT\x9a\xd8\xcbk\xba\x16K"
t.go:111: 2026-03-13 15:20:52.201 [debu] agent.net.tailnet.net.wgengine: magicsock: got derp-999 packet: "\x04\x00\x00\x00C}\xe0\xb9\x02\x00\x00\x00\x00\x00\x00\x00\x1e\x93a\x15\xfev\x81'\xa9?\xe8nR\xce<\x91\x86\xcau@\xb9\xcfɩ\xef\xd1眓\x95\xf3*X^7\x99\x88\xb0|\x8cS\xe4@[\x16\xda\xca\xd4\xd9\x1dP\xd0\xfe\xd9r\x8c\xfcp~dP\xfaK\xe0\xf9y\xb2\x11\x15\xfe\xdcx鷽\xdeF\xf7\x92\xe8"
```
Migration 000434 converts chat_messages.role from text to a Postgres
enum, rebuilds the partial index, and adds content_version smallint.
The column is backfilled with DEFAULT 0, then the default is dropped
so future inserts must set it explicitly.
Version 0 uses the role-aware heuristic from #22958. Version 1 (all
new inserts) stores []ChatMessagePart JSON for all roles, including
system messages. ParseContent takes database.ChatMessage directly
and dispatches on version internally. Unknown versions error.
All string(codersdk.ChatMessageRole*) casts at DB write sites are
replaced with database.ChatMessageRole* constants from sqlc.
Refs #22958
File-reference parts in user messages were flattened to `TextContent` at
write time because fantasy has no file-reference content type. The
frontend never saw them as structured parts.
This moves all write paths (user, assistant, tool) from fantasy envelope
format to `codersdk.ChatMessagePart`. The streaming layer (`chatloop`)
is untouched, the conversion happens at the serialization boundary in
`persistStep`.
Old rows are still readable. `ParseContent` uses a structural heuristic
(`isFantasyEnvelopeFormat`) to distinguish legacy envelopes from SDK
parts. We chose this over try/fallback because fantasy envelopes
partially unmarshal into `ChatMessagePart` (the `type` field matches)
while silently losing content. A guard test enforces that no SDK part
can produce the envelope shape.
This is forward-only: new rows are unreadable by old code. Chat is
behind a feature flag so rollback risk is contained.
Also adds a typed `ChatMessageRole` to replace raw strings and
`fantasy.MessageRole*` casts at the persistence boundary. The type
covers `ChatMessage.Role`, `ChatStreamMessagePart.Role`, the
`PublishMessagePart` callback chain, and all DB write sites.
`fantasy.MessageRole*` remains only where we build `fantasy.Message`
structs for LLM dispatch.
Separately, `ProviderMetadata` was leaking to SSE clients via
`publishMessagePart`. `StripInternal` now runs on both the SSE and REST
paths, covering this.
Other cleanup:
- Old `db2sdk.contentBlockToPart` silently dropped metadata on
text/reasoning/tool-call content. New code preserves it.
- `providerMetadataToOptions` now logs warnings instead of silently
returning nil.
- `db2sdk` shrinks from ~250 lines of parallel conversion to ~15 lines
delegating to `chatprompt.ParseContent()`, removing the `fantasy` import
entirely.
Refs #22821
_Disclaimer: implemented by a Coder Agent using Claude Opus 4.6._
Marks the injected MCP approach in AI Bridge as deprecated across the
codebase.
## Changes
- **`codersdk/deployment.go`**: Deprecated `ExternalAuthConfig.MCPURL`,
`.MCPToolAllowRegex`, `.MCPToolDenyRegex` fields; deprecated and hid the
`--aibridge-inject-coder-mcp-tools` server flag; deprecated
`AIBridgeConfig.InjectCoderMCPTools`.
- **`coderd/externalauth/externalauth.go`**: Deprecated `Config.MCPURL`,
`.MCPToolAllowRegex`, `.MCPToolDenyRegex`.
- **`enterprise/aibridgedserver/aibridgedserver.go`**: Added runtime
deprecation warning when `CODER_AIBRIDGE_INJECT_CODER_MCP_TOOLS` is
enabled; deprecated `getCoderMCPServerConfig`.
- **`enterprise/aibridged/mcp.go`**: Deprecated `MCPProxyBuilder`
interface and `MCPProxyFactory` struct.
- **`docs/ai-coder/ai-bridge/mcp.md`**: Added deprecation warning
banner.
## Summary
Adds a new `GET /api/v2/debug/profile` endpoint that collects multiple
pprof profiles in a single request and returns them as a tar.gz archive.
This allows collecting profiles (including block and mutex) without
requiring `CODER_PPROF_ENABLE` to be set, and without restarting
`coderd`.
Closes#21679
## What it does
The endpoint:
- Temporarily enables block and mutex profiling (normally disabled at
runtime)
- Runs CPU profile and/or trace for a configurable duration (default
10s, max 60s)
- Collects snapshot profiles (heap, allocs, block, mutex, goroutine,
threadcreate)
- Returns a tar.gz archive containing all requested `.prof` files
- Uses an atomic bool to prevent concurrent collections (returns 409
Conflict)
- Is protected by the existing debug endpoint RBAC (owner-only)
**Supported profile types:** cpu, heap, allocs, block, mutex, goroutine,
threadcreate, trace
**Query parameters:**
- `duration`: How long to run timed profiles (default: `10s`, max:
`60s`)
- `profiles`: Comma-separated list of profile types (default:
`cpu,heap,allocs,block,mutex,goroutine`)
## Additional changes
- **SDK client method** (`codersdk.Client.DebugCollectProfile`) for easy
programmatic access
- **`coder support bundle --pprof` integration**: tries the consolidated
endpoint first, falls back to individual `/debug/pprof/*` endpoints for
older servers
- **8 new tests** covering defaults, custom profiles, trace+CPU,
validation errors, authorization, and conflict detection
Docker keeps named volumes around, even if they're dangling. During
development users generate a lot of named coder- volumes and the like.
These are now cleaned on shutdown once they are 30 days old.
For long running workspaces, go build cache also accumulates, so now
we clean these up via cron as well. All cache older than 2 days is wiped
and the cron schedule is based on workspace ID to distribute the load.
## Summary
Moves the messages response out of `GET /chats/{id}` and into a
dedicated `GET /chats/{id}/messages` endpoint.
### Backend
- `GET /chats/{id}` now returns just the `Chat` object (no messages)
- `GET /chats/{id}/messages` is a new endpoint returning
`ChatMessagesResponse` with `messages` and `queued_messages`
- Added `ChatMessagesResponse` SDK type and `GetChatMessages` client
method
### Frontend
- `getChat()` API method returns `Chat` instead of `ChatWithMessages`
- Added `getChatMessages()` API method for the new endpoint
- Split `chatQuery` into two: `chatQuery` (metadata) and
`chatMessagesQuery` (messages)
- Updated all cache mutations, optimistic updates, and websocket
handlers
- Updated tests and stories
### Files changed
| File | Change |
|---|---|
| `coderd/coderd.go` | Register `GET /messages` route |
| `coderd/chats.go` | Simplify `getChat`, add `getChatMessages` handler
|
| `codersdk/chats.go` | New type + method, update `GetChat` return |
| `site/src/api/api.ts` | New method, update `getChat` |
| `site/src/api/queries/chats.ts` | New query, update cache mutations |
| `site/src/pages/AgentsPage/AgentDetail.tsx` | Use separate queries |
| `site/src/pages/AgentsPage/AgentDetail/ChatContext.ts` | Update types
and cache writes |
| `site/src/pages/AgentsPage/AgentsPage.tsx` | Update websocket cache
handler |
## Summary
Adds a microphone button to the agent chat input for browser-native
voice-to-text transcription using the Web Speech API.
## Changes
### New: `site/src/hooks/useSpeechRecognition.ts`
- Custom React hook wrapping the Web Speech API (`SpeechRecognition` /
`webkitSpeechRecognition`)
- Feature-detects browser support via `isSupported`
- Provides `start()`, `stop()`, and `cancel()` controls
- Accumulates real-time transcript from interim and final recognition
results
- Inline TypeScript declarations for the Web Speech API types
### Modified: `site/src/pages/AgentsPage/AgentChatInput.tsx`
- **Mic button**: Appears to the right of the image attach button when
the browser supports the Speech Recognition API. Shows a microphone icon
when idle, X icon when recording.
- **Send button**: Transforms into a checkmark during recording to
accept the transcription. Always enabled during recording.
- **Editor sync**: Live-updates the Lexical editor with the
transcription as the user speaks. Preserves any pre-existing text.
- **Cancel**: Restores the editor to its pre-recording content.
## How it works
1. User clicks the mic button → recording starts, real-time transcript
appears in the editor
2. User clicks the checkmark (send button) → recording stops,
transcribed text stays
3. User clicks X (mic button) → recording stops, editor reverts to
original content
The timeout was started before the unbounded Stepper loop, so
under CI load the deadline could expire before reaching the
operations that actually use it.
Also bumps TestMigration000387 from WaitLong to WaitSuperLong.
Fixescoder/internal#1398
## Summary
Extract a `healthyChecker()` test helper that returns an all-healthy
baseline `testChecker` in `coderd/healthcheck`. Each `TestHealthcheck`
table-driven test case now only overrides the single report field being
tested, instead of repeating all 6 healthy report structs.
- Reduces `healthcheck_test.go` from 603 to 341 lines (~260 lines, 43%
reduction)
- Test coverage unchanged at 77.2%
- All test cases and assertions preserved exactly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- `coderd/httpapi/websocket.go`: add `net.ErrClosed` +
`websocket.CloseStatus` checks; extract `heartbeatCloseWith` with
`quartz.Clock` parameter for testability
- `coderd/httpapi/websocket_internal_test.go`: new test file
## Problem
When a chat is interrupted while tools are executing, the step content
(text, reasoning, tool calls, and partial tool results) was being lost.
Two gaps existed:
1. **During tool execution**: `executeTools` returns with error results
for interrupted tools, but the subsequent `PersistStep(ctx, ...)` fails
on the canceled context and returns `ErrInterrupted` without persisting
anything.
2. **PersistStep race**: If the context is canceled between the
post-tool interrupt check and the `PersistStep` call, the same loss
occurs.
This is inconsistent with how we handle stream interruptions (which
properly flush and persist partial content via `persistInterruptedStep`)
and how [coder/blink](https://github.com/coder/blink) handles
interruptions (always inserting the response message regardless of
execution phase).
## Fix
Two changes in `chatloop.go`:
- **Post-tool-execution interrupt check**: After `executeTools` returns,
check if the context was interrupted and route through
`persistInterruptedStep` (which uses `context.WithoutCancel` internally)
to save the accumulated content.
- **PersistStep fallback**: If `PersistStep` returns `ErrInterrupted`,
retry via `persistInterruptedStep` so partial content is not lost.
## Tests
- `TestRun_InterruptedDuringToolExecutionPersistsStep`: Verifies that
when a tool is blocked and the chat is interrupted, the step (text +
reasoning + tool call + tool error result) is persisted via the
interrupt-safe path.
- `TestRun_PersistStepInterruptedFallback`: Verifies that when
`PersistStep` itself returns `ErrInterrupted`, the step is retried via
the fallback path and content is saved.