Commit Graph
13820 Commits
Author SHA1 Message Date
Jon Ayers 6c44de951d feat: add Prometheus collector for DERP server expvar metrics (#22583)
This PR does three things:
- Exports derp expvars to the pprof endpoint
- Exports the expvar metrics as prometheus metrics in both coderd and
wsproxy
- Updates our tailscale to a fix I also had to make to avoid a data race
condition

I generated this with mux but I also manually tested that the metrics
were getting properly emitted
2026-03-06 01:57:58 -06:00
Kyle Carberry d034903736 fix: clear agents page draft from localStorage after chat creation (#22691)
## Problem

When creating a new chat from the `/agents` page and navigating back,
the initial prompt was still displayed. The draft text persisted in
`localStorage` (`agents.empty-input`) even after the chat was
successfully created.

### Root cause

After `handleSend` synchronously clears the localStorage key, the React
re-render (caused by `isCreating` flipping to `true`) triggers Lexical's
`ContentChangePlugin`, which fires `handleContentChange` with the old
editor content — re-writing the draft back to localStorage before
navigation occurs.

## Fix

- Extract the draft persistence logic into a `useCreatePageDraft` hook
with a `sentRef` guard
- Once `markSent()` is called, `handleContentChange` skips all
localStorage writes
- This prevents the Lexical editor's change events from re-persisting
the draft during the async gap between send and navigation

## Testing

Added 6 unit tests for `useCreatePageDraft`, including a regression test
that reproduces the exact bug scenario (calling `handleContentChange`
with old content after `markSent`).
2026-03-05 21:13:40 -05:00
Matt Vollmer fd60fa7eb6 docs: add platform controls page for Coder Agents (#22680)
Adds a docs page under /docs/ai-coder/agents describing our philosophy
on platform team control over agent behavior: admin-level configuration,
zero developer options, enforcement over defaults. Covers what's
available today (providers, models, system prompt, template routing) and
where we're headed (usage analytics, infra-level enforcement, tool
customization).
2026-03-06 00:23:50 +00:00
Cian Johnston 0b1e4880bd chore(cli): fix TestTokens harder (#22684)
`time.Now()` is greater than microsecond precision while timestamps we
store in Postgres are only microsecond precision. Flake potential is
non-zero.
2026-03-06 00:04:09 +00:00
Danielle Maywood 9f6f4ba74d fix(agents): prevent updated_at regression causing sidebar group flicker (#22672) 2026-03-05 21:21:14 +00:00
Kayla はな 56bdea73b8 feat: add workspace acls to task rbac objects (#22311)
To allow tasks to be shareable, we need to share both the `task`
resource and the `workspace` resource, and their sharing state needs to
be kept in sync. We've already implemented all of the necessary ACL
functionality for workspaces, so we can just sort of proxy those ACLs
back to the task as well.
2026-03-05 13:40:53 -07:00
Mathias Fredriksson 719c24829a build(Makefile): use atomic writes for remaining gen targets (#22670)
Follow-up to #22612. Running `git status --short` in a loop during `make
-B -j gen` still showed intermediate states for several files. This PR
fixes the remaining ones.

The main issues:

- `generate.sh` ran `gofmt` and `goimports` in-place after moving files
  into the source tree. Now it formats in a workdir first and only `mv`s 
  the final result.
- `protoc` targets wrote directly to the source tree. Wrapped with
  `scripts/atomic_protoc.sh` which redirects output to a tmpdir.
- Several generators used hardcoded `/tmp/` paths. On systems where
  `/tmp` is tmpfs, `mv` degrades to copy+delete. Switched to a
  project-local `_gen/` directory (gitignored, same filesystem).
- `apidoc/.gen` and `cli/index.md` used `cp` for final output. Replaced
  with `mv`.
- `manifest.json` was written twice (unformatted, then formatted). Now
  `.gen` writes to a staging file and the manifest target does one
  formatted atomic write.
- `biome_format.sh` silently skipped files in gitignored dirs. Added
  `--vcs-enabled=false`.

Two helpers reduce the Makefile boilerplate: `scripts/atomic_protoc.sh`
(wraps protoc) and an `atomic_write` Make define
(stdout-to-temp-to-target pattern). `.PRECIOUS` now also covers `.pb.go`
and mock files.

Verification: `make -B -j gen` x3 with `git status` polling, no changes.

Refs #22612
2026-03-05 22:32:18 +02:00
Danielle Maywood f91475cd51 test: remove unnecessary dbauthz.AsSystemRestricted calls in tests (#22663) 2026-03-05 20:29:49 +00:00
Jon Ayers 25dac6e5f7 docs: add process priority management documentation (#22626) 2026-03-05 14:16:29 -06:00
Kyle Carberry 51f298f2de fix: render +0 -0 diff stats for zero-line changes (e.g. images) (#22678)
When a git change has zero additions and zero deletions (like adding a
binary file/image), the diff stats were hidden entirely because of
`additions === 0 && deletions === 0` early-return guards.

This changes the behavior so that `+0 -0` is always rendered when there
are changed files, ensuring visibility in both the sidebar and the Git
tab.

### Changes

**`DiffStats.tsx`**
- `DiffStatNumbers`: Removed the `null` early return — always renders
both `+N` and `−N` counters.
- `DiffStatBadge`: Now only returns `null` when there are no changed
files AND both counts are zero. Always renders both pills.
- `DiffStatsInline`: Same guard — shows `+0 −0` clickable stats when
files changed but lines are zero.

**`AgentsSidebar.tsx`**
- `hasLineStats` now also checks `changedFiles > 0`, so the sidebar
entry shows `+0 -0` for binary-only diffs.
- Removed the `additions > 0` / `deletions > 0` conditional wrappers —
both values are always rendered.
2026-03-05 14:13:47 -06:00
George K 5dd570f099 fix(cli/cliui): apply defaults when rendering select prompts (#22093)
The `--parameter-default` value is now used to pre-select the default option for a coder parameter
with option blocks when prompting interactively in CLI.

Related to: https://github.com/coder/coder/issues/22078
2026-03-05 09:35:57 -08:00
Jake Howell dba688662c feat: add <ErrorAlert /> debug details (#22462)
Closes #22140

Short simple and sweet PR to add a bunch of details to our `<Alert />`
stacks. This means we aren't simply asking the user to read the
developer console and surface things easier.

- Implement `Response data` and `Stack trace` `<details />`
- Fix overflow in `ErrorAlert` debug accordions so long `Response data`
and `Stack Trace` content stays inside the alert.
- Add horizontal scroll wrappers around both `<pre>` blocks used in
debug details.
- Update `Alert` layout with `min-w-0` on flex containers so nested
content can shrink correctly and internal scrolling works as intended.

<img width="739" height="550" alt="preview-validation"
src="https://github.com/user-attachments/assets/a6f890d3-8f1f-4fd6-b9d0-882838db04a4"
/>
2026-03-06 03:48:01 +11:00
Danielle Maywood 0ec27e3d48 feat(chatd): navigate to specific chat on push notification click (#22668) 2026-03-05 16:40:17 +00:00
Kyle Carberry 8d3d537ca6 fix: replace padded apple-touch-icon with resized PWA icon (#22674)
The `apple-touch-icon.png` had weird padding/cropping that looked wrong
when added to the iOS home screen. This replaces it with a 180×180
resize of the canonical `pwa-icon-512.png` so the icon matches the PWA
icon exactly — no extra padding.
2026-03-05 16:13:14 +00:00
Kyle Carberry 6520159045 feat(chatd): add start_workspace tool to agent flow (#22646)
## Summary

When a chat's workspace is stopped, the LLM previously had no way to
start it — `create_workspace` would either create a duplicate workspace
or fail. This adds a dedicated `start_workspace` tool to the agent flow.

## Changes

### New: `start_workspace` tool
(`coderd/chatd/chattool/startworkspace.go`)
- Detects if the chat's workspace is stopped and starts it via a new
build with `transition=start`
- Reuses the existing `waitForBuild` and `waitForAgent` helpers (shared
logic)
- Shares the workspace mutex with `create_workspace` to prevent races
- Idempotent: returns immediately if the workspace is already running or
building
- Returns a `no_agent` / `not_ready` status if the agent isn't available
yet (non-fatal)

### Updated: `create_workspace` stopped-workspace hint
- `checkExistingWorkspace` now returns a `stopped` status with message
`"use start_workspace to start it"` when it detects the chat's workspace
is stopped, instead of falling through to create a new workspace

### Wiring
- `chatd.Config` / `chatd.Server`: new `StartWorkspace` /
`startWorkspaceFn` field
- `coderd/chats.go`: new `chatStartWorkspace` method that calls
`postWorkspaceBuildsInternal` with proper RBAC context
- `coderd/coderd.go`: passes `chatStartWorkspace` into chatd config
- Tool registered alongside `create_workspace` for root chats only (not
subagents)

### Tests (`startworkspace_test.go`)
- `NoWorkspace`: error when chat has no workspace
- `AlreadyRunning`: idempotent return for workspace with successful
start build
- `StoppedWorkspace`: verifies StartFn is called, build is waited on,
and success response returned
2026-03-05 15:34:24 +00:00
Zach 26205b9888 docs: add jail type to boundary config docs + sort config options (#22629)
Adds jail_type to boundary config docs and sorts config options alphabetically.
2026-03-05 08:24:59 -07:00
Ethan 5a5828b090 fix(cli): add trailing dot to Coder Connect hostname to prevent DNS search domain expansion (#22607)
## Problem

When `coder ssh --stdio` checks for Coder Connect availability, it
constructs a hostname like `agent.workspace.owner.coder` and performs a
DNS AAAA lookup via `ExistsViaCoderConnect`. Without a trailing dot,
this hostname is not a fully-qualified domain name (FQDN), so the system
DNS resolver appends each configured search domain before querying.

Go's pure-Go DNS resolver (used when `CGO_ENABLED=0`, which is the
default for CLI builds) does **not** stop after getting NXDOMAIN on the
first name. It tries all names in the search list sequentially:

1. `agent.workspace.owner.coder.` → NXDOMAIN (fast)
2. `agent.workspace.owner.coder.corp.example.com.` → timeout
3. `agent.workspace.owner.coder.internal.company.com.` → timeout

On corporate networks where the search-domain-expanded queries hit DNS
infrastructure that drops rather than responds (common for nonsensical
hostnames with deep subdomain chains), each expanded query hits the full
DNS timeout (default 5s × 2 attempts = 10s per name). With 2-3 search
domains, this compounds to 20-30+ seconds of blocking.

## Fix

Adding a trailing dot marks the hostname as an FQDN. Go's `nameList()`
in `src/net/dnsclient_unix.go` returns a single-entry list for rooted
names, completely bypassing search domain expansion.

This is consistent with how `IsCoderConnectRunning` already handles its
DNS check — `tailnet.IsCoderConnectEnabledFmtString` includes a trailing
dot for exactly this reason.

## Verification

Tested with a fake DNS server that responds with NXDOMAIN for `.coder`
queries but drops search-domain-expanded queries:

| Hostname | Time | Queries sent |
|---|---|---|
| `main.workstation.kevin.coder` (no trailing dot) | **~15s** | 4 (as-is
+ 3 search domains) |
| `main.workstation.kevin.coder.` (trailing dot) | **<1ms** | 1 (FQDN
only) |

Closes https://github.com/coder/coder/issues/22581

_Generated by [mux](https://github.com/coder/mux) but reviewed by a
human_
2026-03-06 01:56:54 +11:00
Kyle Carberry be1d58bc6e fix: refocus chat input after message send completes (#22666)
## Problem

The chat input loses focus after sending a message. Users have to click
back into the input field to type their next message.

## Root Cause

When a message is sent:
1. `handleSubmit` in `AgentChatInput` calls `onSend(text)` then
immediately `focus()` — but this is premature
2. The async send sets `isLoading=true`, which disables the editor via
`EditableStatePlugin`
3. When the send resolves, `handleSendFromInput` calls `clear()` and
`focus()` — but the editor may still be disabled at this point (React
hasn't re-rendered yet)
4. When React re-renders with `isLoading=false`, the editor becomes
editable again but nobody restores focus

## Fix

Added a `useEffect` in `AgentChatInput` that watches for `isLoading`
transitioning from `true` to `false` and calls `focus()` on the editor.
This ensures focus is restored *after* React has re-enabled the editor,
not prematurely.

## Test

Added a test in `AgentDetail.test.ts` verifying that `focus()` is called
on the input ref after `handleSendFromInput` resolves.
2026-03-05 09:54:37 -05:00
Kyle Carberry 0d8a0af2b5 fix(site): add WebSocket reconnection with exponential backoff to chat stream (#22662)
## Problem

During rolling deploys, the chat stream WebSocket disconnects and the
user sees **"Chat stream disconnected."** permanently with no recovery
other than a full page refresh.

## Changes

Add automatic WebSocket reconnection with capped exponential backoff (1s
→ 2s → 4s → … → 10s max) to the chat stream `useEffect` in
`ChatContext.ts`.

**`ChatContext.ts`**
- Wrap socket creation in a `connect()` function that can be retried on
disconnect.
- On `error` or `close`, schedule a reconnect with exponential backoff
via `scheduleReconnect()`.
- Guard against double scheduling when both `error` and `close` fire for
the same disconnect (`disconnected` flag per connection).
- On successful reconnect (`open` event), reset backoff and clear
`streamError`.
- Pass the latest `lastMessageIdRef` on each reconnect so the server
replays only unseen durable messages via `after_id`.
- Add `RECONNECT_BASE_MS` (1s) and `RECONNECT_MAX_MS` (10s) constants.

**`ChatContext.test.tsx`**
- Extend mock socket with `emitOpen()` and `emitClose()` helpers.
- Update existing disconnect tests for new reconnect behavior.
- Add **"sets streamError on WebSocket disconnect and reconnects"** —
verifies error banner appears, reconnect fires, and error clears on
open.
- Add **"uses exponential backoff on consecutive disconnects"** —
verifies increasing delays between reconnects.
- Add **"passes latest message ID on reconnect for catch-up"** —
verifies `after_id` is forwarded on each reconnect.

### User experience

- **Before**: "Chat stream disconnected." — permanent, requires page
refresh
- **After**: "Chat stream disconnected. Reconnecting…" — auto-recovers
within seconds

### Limitations

`message_part` events (streaming LLM tokens) are ephemeral / in-memory
only. If the processing replica dies mid-step, those partial tokens are
lost regardless of reconnect. The backend's stale-chat recovery will
re-run the step on a new replica; the reconnect ensures the client is
connected to see that happen.
2026-03-05 09:17:41 -05:00
Kyle Carberry f1b3eef834 feat(site): add PWA manifest and mobile meta tags for agents page (#22650)
Adds progressive web app support for the agents page so it can be
installed as a standalone app on mobile/desktop.

## Changes

- **`manifest.json`** — Web app manifest with `display: standalone`,
`start_url: /agents`, Coder theme colors
- **PWA icons** — 192x192, 512x512 PNGs + 180x180 apple-touch-icon,
rendered from the existing favicon SVG
- **`index.html`** — Added manifest link, apple-touch-icon, and mobile
web app meta tags (`apple-mobile-web-app-capable`,
`mobile-web-app-capable`, `apple-mobile-web-app-status-bar-style`,
title)
- **Service worker** — `notificationclick` now focuses an existing
agents tab or opens `/agents` in a new window

## Testing

1. Open `/agents` on a mobile device
2. Use browser "Add to Home Screen" / "Install App"
3. App should launch in standalone mode pointing at the agents page
4. Push notifications should navigate to the agents page on click
2026-03-05 08:55:38 -05:00
Danielle Maywood c308db805d fix(site): improve git diff number contrast on light theme in agents sidebar (#22661) 2026-03-05 13:38:50 +00:00
Kyle Carberry 76076de1ca feat(site): polish right panel tabs, diff stats, and file headers (#22636)
Polishes the right panel UI introduced in #22633:

<img width="3138" height="1596" alt="image"
src="https://github.com/user-attachments/assets/d3947db0-6600-4469-b7e2-6eb80aadb7bc"
/>

Over 2k lines of this is just the Seti font definition.

The file tree view isn't actually adjusting much, it's just a scroll
helper. Soon I'll add a comment system so users can leave agents
feedback directly from the code.
2026-03-05 07:38:49 -05:00
Mathias Fredriksson a6a8fd94d7 build(Makefile): enable parallel make -j gen with correct dependency graph (#22612)
`make gen` could not run with `-j` because inter-target dependency edges
were missing. Multiple recipes compile `coderd/rbac` (which includes
generated files like `object_gen.go`), and without explicit ordering,
parallel runs produced syntax errors from mid-write reads.

Three main changes:

**Dependency graph fixes** declare the compile-time chain through
`coderd/rbac` so that `object_gen.go` is written before anything that
imports it is compiled. The DB generation targets use a GNU Make 4.3+
grouped target (`&:`) so Make knows `generate.sh` co-produces
`querier.go`, `unique_constraint.go`, `dbmetrics`, and `dbauthz` in a
single invocation. `SKIP_DUMP_SQL=1` avoids re-entrant `make` inside
`generate.sh` when the Makefile already guarantees `dump.sql` is fresh.

**`scripts/atomicwrite` package** replaces `os.WriteFile` in all gen
scripts with a temp-file-in-same-dir + rename pattern, preventing
interrupted runs from leaving partial files.

**`.PRECIOUS` and shell atomic writes** protect git-tracked generated
files from Make's default delete-on-error behavior. Since these files
are committed, deletion is worse than staleness -- `git restore` is the
recovery path.

CI now runs `make -j --output-sync -B gen` (~32s, down from ~85s
serial).

| Scenario                          | Before             | After    |
|-----------------------------------|--------------------|----------|
| `make gen` (serial)               | 95s                | 95s      |
| `make -j gen` (parallel)          | race error         | **22s**  |
| CI `make -j --output-sync -B gen` | forced serial ~85s | **~32s** |
2026-03-05 11:58:10 +00:00
Danielle Maywood b0e10402c8 feat(site): searchable workspace selector in agent chat input (#22656) 2026-03-05 11:38:45 +00:00
Cian Johnston 89cee2dd81 chore(cli): fix flaky temporal assertion in TestTokens (#22654)
Fixes https://github.com/coder/internal/issues/1379
2026-03-05 10:18:51 +00:00
Danielle Maywood 076a482689 fix(site): prevent dropdown portal flash in agents sidebar (#22632) 2026-03-05 10:18:40 +00:00
Danielle Maywood 024d07350a fix(site): convert div role="button" to native button in ai-elements tools (#22631) 2026-03-05 09:58:37 +00:00
Cian JohnstonandAtif Ali 9c91f472b9 ci: remove temporary deploy override (#22389)
Merge after Tuesday March 3rd.

Co-authored-by: Atif Ali <atif@coder.com>
2026-03-05 09:47:15 +00:00
Cian Johnston d0a51e1752 fix: use testutil.Eventually in chatd interrupt test (#22653)
Follow-up to #22630. Addresses [review
feedback](https://github.com/coder/coder/pull/22630#pullrequestreview-2953419963)
that was missed due to auto-merge.

## Changes

Replaces three `require.Eventually` calls with `testutil.Eventually` in
`TestInterruptChatDoesNotSendWebPushNotification`, linking the condition
to the existing test context (`ctx`) created on line 1194. This ensures
the test respects context cancellation instead of using a standalone
timeout/tick pattern.
2026-03-05 09:42:34 +00:00
Cian Johnston 4d0d187806 fix(chatd): wait for startup scripts before returning from create_workspace (#22498)
The `create_workspace` tool waited for the workspace build to succeed
and the agent to become connectable, but did not wait for the agent's
startup scripts (e.g. git clone) to finish. This caused agents to
attempt file operations on repositories that hadn't been cloned yet.

Add a waitForStartupScripts step that polls the agent's lifecycle_state
via GetWorkspaceAgentLifecycleStateByID until it transitions out of
created/starting into a terminal state (ready, start_error, or
start_timeout). The tool now only returns success once the workspace is
fully initialized.

If the scripts fail or time out, the tool still returns (non-fatal) with
an appropriate agent_status so the model knows something went wrong.

Created using thingies (Opus 4.6 Max)
2026-03-05 09:42:12 +00:00
Susana Ferreira 9f83eb1544 docs: add TLS listener configuration for AI Bridge Proxy (#22548)
## Description

Documents the new TLS listener support for AI Bridge Proxy.

Updates `setup.md` with a new "Proxy TLS Configuration" section covering self-signed and corporate CA certificate setup, rewrites "Security Considerations" to reflect TLS as the recommended approach for encrypting client connections, and updates "Client Configuration" with `HTTPS_PROXY` defaults and combined certificate trust instructions.

Updates `copilot.md` to default all proxy URL examples to `https://`, add TLS certificate trust guidance for each client (CLI, VS Code, JetBrains), and document the MCP server trust store requirement for Copilot CLI.

Closes: https://github.com/coder/internal/issues/1335
2026-03-05 09:26:32 +00:00
Susana Ferreira 21c91cebaa feat: add TLS listener support to aibridgeproxyd (#22411)
## Description

Adds optional TLS support for the AI Bridge Proxy listener. When TLS cert and key files are provided, the proxy serves over HTTPS instead of plain HTTP.

## Changes

* New configuration options to enable TLS on the proxy listener 
* Wraps the TCP listener in `tls.NewListener` when configured
* Tests for validation errors, invalid files, and full integration (tunneled + MITM) through a TLS listener

Note: Documentation for TLS listener setup and client configuration will be handled in a follow-up PR.
Related to: https://github.com/coder/internal/issues/1335
2026-03-05 09:19:34 +00:00
Kyle Carberry 7bcd9f6de8 fix: skip web push notification when chat is interrupted (#22630)
When a user interrupts a chat, the status transitions to `waiting` which
previously triggered an "Agent has finished running." web push
notification. This is incorrect — the user interrupted it themselves, so
no notification is needed.

## Changes

### `coderd/chatd/chatd.go`
- Added `wasInterrupted` flag alongside the existing `status` variable
- Set the flag when `ErrInterrupted` is detected in the error handler
- Added `!wasInterrupted` to the web push dispatch condition

### `coderd/chatd/chatd_test.go`
- Added `TestInterruptChatDoesNotSendWebPushNotification` that creates a
chat with a mock webpush dispatcher, processes it, interrupts it, and
verifies no push notification was dispatched
- Added `mockWebpushDispatcher` implementing the `webpush.Dispatcher`
interface
2026-03-05 09:08:17 +00:00
Susana Ferreira c79e8f2707 refactor: clarify MITM certificate naming in aibridgeproxyd (#22408)
## Description

Renames internal fields, variables, and comments related to the proxy's certificate/key configuration to explicitly reference their MITM CA purpose.

The AI Bridge Proxy uses a CA certificate to sign dynamically generated leaf certificates during MITM interception of HTTPS traffic from AI clients. With the upcoming introduction of TLS listener certificates (for serving the proxy itself over HTTPS, implemented upstack https://github.com/coder/coder/pull/22411), the previous generic naming would become ambiguous. This refactor makes it clear which certificate is which.

No user-facing flags, environment variables, YAML keys, or JSON fields were changed, this is purely an internal rename to avoid confusion going forward.

Related to https://github.com/coder/internal/issues/1335
2026-03-05 09:06:38 +00:00
Kyle Carberry 94a2e440a8 fix(chatd): extract session token from cookie for relay header (#22649)
## Problem

When a browser connects to the chat stream via WebSocket, it
authenticates using cookies only — the native WebSocket API cannot set
custom headers like `Coder-Session-Token`. The relay between replicas
copies the original request's `Cookie` header but did **not** set the
`Coder-Session-Token` header as a fallback.

This causes a **401 on the worker replica** when `EnableHostPrefix` is
enabled, because the `HTTPCookies.Middleware` strips bare
`coder_session_token` cookies (expecting the `__Host-` prefix). Without
a `Coder-Session-Token` header fallback, `apiKeyMiddleware` finds no
valid credentials.

### Root Cause

The data flow:
1. Browser → subscriber replica: `Cookie:
__Host-coder_session_token=xxx` (browser sends prefixed cookie)
2. Subscriber's `HTTPCookies.Middleware` normalizes: `Cookie:
coder_session_token=xxx` (strips prefix)
3. `relayHeaders()` copies `Cookie: coder_session_token=xxx` to relay
request
4. Worker replica's `HTTPCookies.Middleware` sees bare
`coder_session_token` → **strips it** (expects `__Host-` prefix)
5. `apiKeyMiddleware` → `APITokenFromRequest`: no cookie, no header →
**401**

## Fix

Modified `relayHeaders()` to extract the session token value from the
`Cookie` header and set it as the `Coder-Session-Token` header when no
explicit session token header is already present. The header is never
stripped by middleware, so the worker replica can always authenticate.

## Testing

- **`TestRelayHeaders`**: Unit tests for the updated `relayHeaders()`
function covering all scenarios (cookie-only, header+cookie, no auth,
nil source)
- **`TestExtractSessionTokenFromCookieHeader`**: Unit tests for the
helper function
- **`TestChatStreamRelay/RelayCookieOnlyAuth`**: Integration test with
plain HTTP, cookie-only WebSocket auth
- **`TestChatStreamRelay/RelayCookieOnlyAuthWithHostPrefix`**:
Integration test with `EnableHostPrefix=true`, confirming the 401 is
fixed
- **`cookieOnlySessionTokenProvider`**: Test helper that simulates
browser WebSocket behavior (sets Cookie header only on WebSocket dials,
no custom headers)

## Files Changed

- `enterprise/coderd/chatd/chatd.go` — `relayHeaders()` fix +
`extractSessionTokenFromCookieHeader()` helper
- `enterprise/coderd/chatd/relay_headers_internal_test.go` — unit tests
(new file)
- `enterprise/coderd/chats_test.go` — integration tests + test helper
type
2026-03-05 05:11:07 +00:00
Jake Howell f609de860f feat: defer api key generation with mutation (#22318)
Closes #22065

This pull-request ensures that when we load the `<WorkspacePage />`
we're not instantly attempting to generate an `apiKey` every-time. These
are now only generated once the user attempts to actually click on the
VSCode link, this is now a mutation also (which is the correct action
for this).
2026-03-05 15:50:28 +11:00
Matt Vollmer 06105c9c62 docs(agents): convert markdown images to HTML img tags (#22647) 2026-03-04 23:21:08 -05:00
Kyle Carberry b28958cef9 Revert "fix(chatd): sanitize \u0000 from JSON before JSONB insertion" (#22645)
Reverts coder/coder#22637
2026-03-05 03:35:52 +00:00
Kyle Carberry 219d02bdc3 fix(coderd): poll for metrics in TestWorkspaceProvisionerdServerMetrics (#22644)
## Problem

`TestWorkspaceProvisionerdServerMetrics` flakes because metric
assertions run immediately after
`AwaitWorkspaceBuildJobCompleted` returns, but metrics are updated
**asynchronously after the
DB transaction commits** in `completeWorkspaceBuildJob`.

The timeline in the provisioner server:
1. DB transaction commits (`provisionerdserver.go:~2362`) — job marked
completed
2. Audit logging, notifications, DB queries (`~2370-2427`)
3. **Metric `.Observe()`** (`~2463`) — happens ~100 lines later

The test synchronization (`AwaitWorkspaceBuildJobCompleted`) polls for
`CompletedAt != nil`,
which fires at step 1. The metric assertion then executes before step 3,
causing the flake.

## Fix

Wrap all three metric assertions (prebuild creation, prebuild claim,
regular workspace
creation) in `require.Eventually` to poll until the metric appears, then
assert on the value.

## Test

- `go test -run TestWorkspaceProvisionerdServerMetrics -count=5` — all
pass
- `go test -race -run TestWorkspaceProvisionerdServerMetrics -count=1` —
clean
2026-03-04 22:30:36 -05:00
Kyle Carberry 5630390d94 fix(chatd): enable compaction between steps and re-enter after summarization (#22640)
## Problem

Three bugs with chat summarization (compaction) share a single root
cause: `ReloadMessages` was never wired up in the production
`chatloop.Run()` call.

### Bug 1: Compaction never fires between steps

The inline compaction guard in `chatloop.go` requires both `Compaction`
and `ReloadMessages` to be non-nil:

```go
if opts.Compaction != nil && opts.ReloadMessages != nil {
```

Since `ReloadMessages` was only set in tests, inline compaction was
**dead code in production**. Long multi-step turns could blow through
the context window.

### Bug 2: Compaction only occurs at end of turn

The post-run safety net doesn't check `ReloadMessages`, so it was the
only compaction path that fired:

```go
if !alreadyCompacted && opts.Compaction != nil { // no ReloadMessages check
```

This meant compaction only happened once, after the entire agent turn
finished.

### Bug 3: Agent stops after summarization

After post-run compaction, `Run()` unconditionally returned `nil`.
`processChat` then set the chat status to `waiting` (done). The agent
never had a chance to continue with its fresh summarized context.

## Fix

1. **Wire up `ReloadMessages`** in `chatd.go`: reloads persisted
messages from the database and re-applies system prompts (subagent
instruction, workspace AGENTS.md).

2. **Wrap the step loop in an outer compaction loop**: when compaction
fires on the model's final step (`compactedOnFinalStep`), reload
messages and `continue` the outer loop so the agent re-enters with
summarized context.

3. **Track `compactedOnFinalStep`** to distinguish inline compaction on
the last step (needs re-entry) from inline compaction mid-loop followed
by more tool-call steps (agent already consumed the compacted context,
no re-entry needed).

4. **Add `maxCompactionRetries = 3`** to prevent infinite compaction
loops.

## Testing

- All 7 existing compaction tests pass unchanged.
- Added `PostRunCompactionReEntersStepLoop` test: verifies that when a
text-only response triggers compaction, the outer loop re-enters and the
agent makes a second stream call with fresh context.
2026-03-04 22:28:23 -05:00
Jake Howell 3fca20df65 feat: move <UserAutocomplete /> to <Combobox /> (#22590)
This pull-request takes our super MUI-based `<UserAutocomplete />` and
migrates it to a common `<Combobox />` so that things will render
correctly inline and use the new `shadcn` style system.

| Old | New |
| --- | --- |
| <img width="1139" height="220"
src="https://github.com/user-attachments/assets/046b32d0-702c-4f03-b039-91f4b1e30cd1"
/> | <img width="1139" height="220"
src="https://github.com/user-attachments/assets/74244768-198a-4a28-acb9-a93ff570e010"
/> |
2026-03-05 14:22:37 +11:00
Kyle Carberry 63b6868113 fix(codersdk): propagate HTTPClient to websocket.Dial for TLS relay (#22642)
## Problem

In multi-replica Coder deployments, the chat relay WebSocket between
replicas fails with HTTP 401 (or TLS handshake errors). The subscriber
replica cannot relay `message_part` events from the worker replica.

**Root cause:** `codersdk.Client.Dial()` does not pass `c.HTTPClient` to
`websocket.DialOptions.HTTPClient`. The websocket library
(`github.com/coder/websocket`) falls back to `http.DefaultClient`, which
lacks the mesh TLS configuration needed for inter-replica communication.

The relay code in `enterprise/coderd/chatd/chatd.go` correctly sets
`sdkClient.HTTPClient = cfg.ReplicaHTTPClient` (which has mesh TLS
certs), but that client was never used for the actual WebSocket
handshake.

## Fix

One-line fix in `codersdk/client.go`: propagate `c.HTTPClient` to
`opts.HTTPClient` when the caller hasn't already set one.

## Test

Added `TestChatStreamRelay/RelayWithTLSAndCookieAuth` which:
- Sets up two replicas with TLS certificates (simulating mesh TLS in
production)
- Authenticates via cookies (simulating browser WebSocket behavior)
- Verifies message_part events relay across replicas over TLS

This test times out without the fix because the WebSocket handshake
fails with `x509: certificate signed by unknown authority`
(http.DefaultClient rejects self-signed certs).

## Related

Follow-up to #22635 which fixed the `redirectToAccessURL` middleware
bypassing 307 redirects for relay requests. That fix changed the error
from HTTP 200 to HTTP 401, exposing this deeper issue.
2026-03-04 21:57:23 -05:00
Matt Vollmer c0995ed736 docs: add Models page and restructure agents docs into directory (#22643)
Adds a Models page documenting LLM provider and model configuration for
Coder Agents. Moves agents pages into `docs/ai-coder/agents/` directory.
URLs are unchanged.

<img width="1343" height="633" alt="image"
src="https://github.com/user-attachments/assets/e870340b-9ae5-4904-9936-49f51ab0e0c4"
/>
2026-03-04 21:56:15 -05:00
Kyle Carberry 27f0f2962c fix(chatd): sanitize \u0000 from JSON before JSONB insertion (#22637)
## Problem

Users hit this error when agent tool results contain Unicode null
characters:

```
persist step: insert tool result: pq: unsupported Unicode escape sequence
```

PostgreSQL's `jsonb` type rejects `\u0000` (Unicode null, U+0000) with
that error, even though it's valid JSON per RFC 8259. Tool results from
agents can contain this sequence — e.g. binary data, C-style strings, or
certain API responses.

## Root cause

`MarshalToolResult` and `MarshalContent` in `chatprompt.go` serialize
content blocks to JSON and pass them directly to `InsertChatMessage`
which casts to `::jsonb`. Go's `json.Marshal` / `json.Valid` accept
`\u0000`, but Postgres does not.

## Fix

Added `sanitizeJSONForPG()` which strips `\u0000` escape sequences from
serialized JSON before insertion. Uses `bytes.Contains` as a fast-path
check to avoid allocation when no null bytes are present (the common
case).

Applied to both `MarshalContent` (assistant messages) and
`MarshalToolResult` (tool result messages).
2026-03-04 21:14:41 -05:00
Kyle Carberry d50fc374c5 fix(coderd): fix flaky TestGetUserStatusCounts timezone boundary (#22639)
## Problem

`TestGetUserStatusCounts/OK_when_offset_is_provided_without_timezone`
fails intermittently in CI:

```
Error:      Should be zero, but was 1
Test:       TestGetUserStatusCounts/OK_when_offset_is_provided_without_timezone
```

## Root Cause

The `happyResponseCheck` asserts `count=0` for all 61 dates. The test
creates a first user, which inserts a `user_status_changes` row with
`new_status=active` and `changed_at=now()`.

The query computes its date range using the requested timezone/offset:

```go
nextHourInLoc = dbtime.Now().Truncate(time.Hour).Add(time.Hour).In(loc)
sixtyDaysAgo  = dbtime.StartOfDay(nextHourInLoc).AddDate(0, 0, -60)
```

When the UTC time of day is earlier than the timezone offset (e.g. UTC
01:30 with offset `-2` means local time is 23:30 previous day),
`StartOfDay(nextHourInLoc)` rounds forward to start-of-today in the
target timezone, which is *after* the current UTC time. The last
`date_of_interest` in the SQL query ends up ahead of `now()` in UTC, so
the user's `changed_at` satisfies `changed_at <= date` — producing
`count=1` on the last date.

This happens ~8% of the time for offset `-2` (when UTC hour is 0 or 1)
and ~15% for `America/St_Johns` (UTC-3:30).

## Fix

Allow the last date entry to have count 0 or 1 (only 1 user exists)
while keeping all earlier dates strictly zero. This correctly accounts
for the timezone boundary without weakening the test's structural
validation.
2026-03-04 18:01:56 -08:00
Kyle Carberry a6b9a25f82 fix(cli): bypass access URL redirect for inter-replica chat relay (#22635)
## Summary

Fixes cross-replica chat relay failing with:

```
failed to open initial relay for chat stream
    error= dial relay stream: - failed to WebSocket dial: expected handshake response status code 101 but got 200

failed to open relay for message parts
    error= dial relay stream: - failed to WebSocket dial: expected handshake response status code 101 but got 200
```

Subscribers see accurate `status=running` (delivered via pubsub) but
miss all in-progress `message_part` events (delivered only via the relay
WebSocket that never connects).

## Root cause

`redirectToAccessURL` in `cli/server.go` redirects any request whose
`Host` header doesn't match the access URL. The enterprise chat relay
dials another replica directly via its DERP relay address (e.g.
`http://10.0.0.2:8080`), so the `Host` header is the pod IP — not the
access URL.

This triggers a **307 redirect** to the access URL. The WebSocket
library follows the redirect, but the second request is a plain GET —
`Connection: Upgrade` and `Upgrade: websocket` headers are **not carried
over** by HTTP redirect semantics. The load-balanced access URL routes
the plain GET to any replica, which serves the SPA catch-all handler and
returns **HTTP 200 with `index.html`**.

The WebSocket library then fails: `expected handshake response status
code 101 but got 200`.

DERP mesh already has an exemption for this exact scenario
(`isDERPPath`). Chat relay was added later and didn't get one.

## Fix

Bypass `redirectToAccessURL` for requests that carry the
`X-Coder-Relay-Source-Replica` header, which the enterprise relay
already sets on every request (`enterprise/coderd/chatd/chatd.go:573`).

## Sequence diagram

**Before (broken):**
```
Replica A (subscriber)          Replica B (worker)           Load Balancer
       |                              |                           |
       |--- WS dial pod-ip:8080 ----->|                           |
       |                              |-- 307 redirect to LB --->|
       |                              |                           |
       |<----------- plain GET (no Upgrade headers) ------------->|
       |                              |                           |-- routes to any replica
       |<----------- 200 index.html -------------------------------|
       |                              |
       X  'expected 101 but got 200'  |
```

**After (fixed):**
```
Replica A (subscriber)          Replica B (worker)
       |                              |
       |--- WS dial pod-ip:8080 ----->|
       |    (X-Coder-Relay-Source-    |
       |     Replica header set)      |
       |                              |-- bypass redirect
       |<--------- 101 Upgrade ------|
       |<==== message_part events ====|
```
2026-03-04 20:26:03 -05:00
Kyle Carberry 6afcc7b904 refactor(site): replace DiffRightPanel with generic tabbed RightPanel (#22633)
Replace the single-purpose DiffRightPanel with a generic RightPanel
component that supports tabs, drag-resize, drag-to-snap, and
drag-to-collapse-sidebar.

## Changes

- **New `RightPanel.tsx`**: generic tabbed panel with:
  - Drag handle with pointer capture for smooth resizing
  - Snap thresholds: drag past max → expand, drag below min → close
- Live sidebar collapse when dragging to the left viewport edge (and
reverses if dragged back)
  - Persisted width via localStorage
- `onVisualExpandedChange` callback so parent syncs sibling visibility
during drag (not just on pointer-up)
- **Deleted `DiffRightPanel.tsx`**
- **Updated `AgentDetail.tsx`**: uses `RightPanel` with `tabContent`
record, tracks `dragVisualExpanded` for live chat section hiding
- **Updated `FilesChangedPanel.tsx`**: removed border/background (now
handled by RightPanel wrapper)

## Drag behavior

| Gesture | Effect |
|---|---|
| Drag left past 70vw + 80px | Snap to expanded (fullscreen within
parent) |
| Drag right below 360px - 80px | Snap to closed |
| Drag to left viewport edge (<80px) | Collapse sidebar live |
| Drag back from left edge | Uncollapse sidebar live |
| Start expanded, drag right | Live resize back to normal |
2026-03-05 00:37:17 +00:00
Matt Vollmer 28d99e8afb docs: add Coder Agents architecture deep-dive page (#22625)
Adds a new child page under **Coder Agents**
(`/docs/ai-coder/agents-architecture`) that explains how the agent in
the control plane communicates with workspaces.

## Core message

The Coder Agent interacts with workspaces using the exact same
connection path as a developer's IDE, web terminal, or SSH session — no
special protocol, no sidecar, no new ports.
2026-03-04 19:17:02 -05:00
Kyle Carberry 30d534b36b fix(chatd): fix relay race conditions, extract enterprise relay logic, move pubsub to OSS (#22589)
## Summary

Fixes a bug where interrupting a streaming chat and sending a new
message
left the relay connected to the wrong replica. Expanded into a broader
refactor that cleanly separates concerns:

- **OSS** owns pubsub subscription, message catch-up, queue updates,
  status forwarding, and local parts merging.
- **Enterprise** (`enterprise/coderd/chatd`) only manages relay dialing,
  reconnection, and stale-dial discarding for cross-replica streaming.

## Architecture

### OSS `coderd/chatd/chatd.go`

`Subscribe()` builds the initial snapshot then runs a single merge
goroutine that handles:

- Pubsub subscription for durable events (status, messages, queue,
errors)
- Message catch-up via `AfterMessageID`
- Local `message_part` forwarding
- Relay events from enterprise (when `SubscribeFn` is set)
- Sends `StatusNotification` to enterprise so it can manage relay
lifecycle

Key types:

- `SubscribeFn` — enterprise hook, returns relay-only events channel
- `SubscribeFnParams` — `ChatID`, `Chat`, `WorkerID`,
`StatusNotifications`, `RequestHeader`, `DB`, `Logger`
- `StatusNotification` — `Status` + `WorkerID`, sent to enterprise on
pubsub status changes

### Enterprise `enterprise/coderd/chatd/chatd.go`

`NewMultiReplicaSubscribeFn(cfg MultiReplicaSubscribeConfig)` returns a
`SubscribeFn` that:

- Opens an initial synchronous relay if the chat is running on a remote
worker
- Reads `StatusNotifications` from OSS to open/close relay connections
- Handles async dial, reconnect timers, stale-dial discarding
- Returns only relay `message_part` events

## Bug fixes

### Original bug: stale relay dial after interrupt

`openRelayAsync` goroutines used `mergedCtx` (subscription-level), not a
per-dial context. `closeRelay()` could not cancel in-flight dials. When
the user interrupts and a new replica picks up the chat, the old dial
goroutine could complete after the new one and deliver a stale
`relayResult`.

**Fix**: per-dial `dialCtx`/`dialCancel`, `expectedWorkerID` tracking,
`workerID` on `relayResult`. `closeRelay()` cancels the dial context and
drains `relayReadyCh`. Merge loop rejects mismatched worker IDs.

### Additional fixes

- `statusNotifications` send-on-closed-channel race — goroutine now owns
  `close()` via defer
- Enterprise spin-loop on `StatusNotifications` close — two-value
receive
  with nil-out
- `hasPubsub` set from `p.pubsub != nil` instead of subscription success
  — now tracks actual subscription result
- `lastMessageID` not initialized from `afterMessageID` — caused
  duplicate messages on catch-up
- `wrappedParts` goroutine leaked remote connection on `dialCtx` cancel
- `closeRelay()` did not drain `relayReadyCh`
- `setChatWaiting` race with `SendMessage(Interrupt)` — wrapped in
`InTx`
- `processChat` post-TX side effects fired when chat was taken by
another
  worker — added `errChatTakenByOtherWorker` sentinel
- Cancel closure data race on `reconnectTimer`
- Bare blocking send on pubsub error path
- `localParts` hot-spin after channel close
- No-pubsub branch dropped relay events and initial snapshot
- Failed relay dial caused permanent stall (no reconnect retry)
- DB error during reconnect timer caused permanent stall
- `time.NewTimer` replaced with `quartz.Clock` for testable timing

## Tests

9 enterprise tests covering:

- Relay reconnect on drop (mock clock)
- Async dial does not block merge loop
- Relay snapshot delivery
- Stale dial discarded after interrupt
- Cancel during in-flight dial
- Running-to-running worker switch
- Failed dial retries (mock clock)
- Local worker closes relay
- Multiple consecutive reconnects (mock clock)

All pass with `-race`.
2026-03-04 18:42:28 -05:00
Kyle Carberry 0ccfc4da06 feat(site): add specialized renderer for process_output tool in agent chats (#22628)
Adds a `ProcessOutputTool` component that renders `process_output` tool
calls with a clean terminal-style output block instead of falling
through to the generic JSON renderer.

## Changes

**New file:** `ProcessOutputTool.tsx`
- Output shown directly with no header
- Copy button and status indicators float top-right on hover
- Collapsible output with the same expand/collapse chevron bar used by
`ExecuteTool`
- Exit code badge shown only for non-zero exits
- Spinner shown while process is still running

**Modified files:**
- `Tool.tsx` — `ProcessOutputRenderer` + registered in `toolRenderers`
map
- `ToolIcon.tsx` — `process_output` falls through to `TerminalIcon`
- `ToolLabel.tsx` — shows "Reading process output" label
2026-03-04 18:04:34 -05:00