Commit Graph
13395 Commits
Author SHA1 Message Date
Michael Suchacz 73f6cd8169 feat: suffix-based chat agent selection (#23741)
Adds suffix-based agent selection for chatd. Template authors can direct
chat traffic to a specific root workspace agent by naming it with the
`-coderd-chat` suffix (for example, `coder_agent "dev-coderd-chat"`).
When no suffix match exists, chatd falls back to the first root agent by
`DisplayOrder`, then `Name`. Multiple suffix matches return an error.

The selection logic lives in `coderd/x/chatd/internal/agentselect` and
is shared by chatd core plus the workspace chat tools so all chat entry
points pick the same agent deterministically.

No database migrations, API contract changes, or provider changes. The
experimental sandbox template was split out to #23777.
2026-03-30 11:43:59 +00:00
Danielle Maywood 4c97b63d79 fix(site/src/pages/AgentsPage): toast when git refresh fails due to disconnection (#23779) 2026-03-30 12:35:23 +01:00
Jakub DomerackiandClaude Sonnet 4.6 28484536b6 fix(enterprise/aibridgeproxyd): return 403 for blocked private IP CONNECT attempts (#23360)
Previously, when a CONNECT tunnel was blocked because the destination
resolved to
a private/reserved IP range, the proxy returned 502 Bad Gateway —
implying an
upstream failure rather than a deliberate policy block.

Introduce `blockedIPError` as a sentinel type returned by both
`checkBlockedIP`
and `checkBlockedIPAndDial`. `ConnectionErrHandler` now inspects the
error with
`errors.As` and returns 403 Forbidden for policy blocks, keeping 502 for
genuine
dial failures.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 12:25:33 +02:00
Danielle Maywood 7a5fd4c790 fix(site): align plus menu icons and add small switch variant (#23769) 2026-03-30 11:18:18 +01:00
Jake Howell 8f73e46c2f feat: automatically generate beta features (#23549)
Closes #15129

Adds a generated **Beta features** table on the feature stages doc,
using the same mainline/stable sparse-checkout approach as the
early-access experiments list.

- Walk `docs/manifest.json` for routes with `state: ["beta"]` and render
a table (title, description, mainline vs stable).
- Inject output between `<!-- BEGIN: available-beta-features -->` /
`END` in `docs/install/releases/feature-stages.md`.
- Rename `scripts/release/docs_update_experiments.sh` →
`docs_update_feature_stages.sh`, refresh the script header, and use
`build/docs/feature-stages` for clone output.

<img width="1624" height="1061" alt="image"
src="https://github.com/user-attachments/assets/5fa811dd-9b80-446b-ae65-ec6e6cfedd6a"
/>
2026-03-30 21:14:52 +11:00
Atif Ali 56171306ff ci: fix SLSA predicate schema in attestation steps (#23768)
Follow-up to #23763.

The custom predicate uses the **SLSA v0.2 schema** (`invocation`,
`configSource`, `metadata`) but declares `predicate-type` as v1.
GitHub's attestation API rejects the mismatch:

```
Error: Failed to persist attestation: Invalid Argument -
predicate is not of type slsa1.ProvenancePredicate
```

This was masked before #23763 because the steps failed earlier on
missing `subject-digest`. Now that digests are provided, this is the
next error.

## Fix

Remove the custom `predicate-type` and `predicate` inputs. Without them,
`actions/attest@v4` auto-generates a correct SLSA v1 predicate from the
GitHub Actions OIDC token — which is what `gh attestation verify`
expects.

- `ci.yaml`: 3 attestation steps (main, latest, version-specific)
- `release.yaml`: 3 attestation steps (base, main, latest)

<details>
<summary>Verification (source code trace of actions/attest@v4)</summary>

1. **`detect.ts`**: No `predicate-type`/`predicate` → returns
`'provenance'` (not `'custom'`)
2. **`main.ts`**: `getPredicateForType('provenance')` →
`generateProvenancePredicate()`
3. **`@actions/toolkit/.../provenance.ts`**:
`buildSLSAProvenancePredicate()` fetches OIDC claims, builds correct v1
predicate with `buildDefinition`/`runDetails`

</details>

> 🤖 This PR was created with the help of Coder Agents, and needs a human
review. 🧑💻
2026-03-30 15:07:13 +05:00
Danielle Maywood 0b07ce2a97 refactor(site): move AgentChatPageView to correct directory (#23770) 2026-03-30 10:49:21 +01:00
Ethan f2a7fdacfe ci: don't cancel in-progress linear release runs on main (#23766)
The Linear Release workflow had `cancel-in-progress: true`
unconditionally, so a new push to `main` would cancel an already-running
sync. This meant successive PR merges would show you a bunch of red Xs
on CI, even though nothing was wrong.

<img width="958" height="305" alt="image"
src="https://github.com/user-attachments/assets/1bd06948-ef2d-469f-9d48-a82277a6110c"
/>

Other workflows like CI guard against this with `cancel-in-progress: ${{
github.ref != 'refs/heads/main' }}`.

This PR does the same thing to the linear release workflow. The job will
be queued instead.

<img width="678" height="105" alt="image"
src="https://github.com/user-attachments/assets/931e38c8-3de4-40d6-b156-d5de5726d094"
/>

Letting the job finish is not particularly wasteful or anything since
the sync takes 30~ seconds in CI time.
2026-03-30 20:46:21 +11:00
Jaayden Halko 0e78156bcd fix: create scrollable proxy menu list (#23764)
This allows the proxy menu to scroll for very large numbers of proxies
in the menu.

<img width="334" height="802" alt="screenshot"
src="https://github.com/user-attachments/assets/f0e45b9c-5b77-43da-b566-28d0572fd56b"
/>
2026-03-30 09:39:30 +01:00
Atif Ali bc5e4b5d54 ci: fix broken GitHub attestations and update SBOM tooling (#23763)
## Problem

GitHub SLSA provenance attestations have been silently failing on
**every release** since they were introduced. Confirmed across all 10+
release runs checked (v2.29.2 through v2.31.6).

The `actions/attest` action requires `subject-digest` (a `sha256:...`
hash) to identify the artifact being attested, but the workflow only
provided `subject-name` (the image tag like
`ghcr.io/coder/coder:v2.31.6`). This caused every attestation step to
error with:

```
Error: One of subject-path, subject-digest, or subject-checksums must be provided
```

The failures were masked by `continue-on-error: true` and only surfaced
as `##[warning]` annotations that nobody noticed. Enterprise customers
doing `gh attestation verify` would find no provenance records for any
of our Docker images.

> [!NOTE]
> The cosign SBOM attestation (separate step) has been working correctly
the entire time — it uses a different mechanism (`cosign attest --type
spdxjson`) that does not require the same inputs. This fix is
specifically for the GitHub-native SLSA provenance attestations.

## Fix

**Add `subject-digest` to all `actions/attest` steps** (release.yaml +
ci.yaml):
- Base image: capture digest from `depot/build-push-action` output
- Main image: resolve digest via `docker buildx imagetools inspect
--raw` after push
- Latest image: same approach
- Use `subject-name` without tag per the [actions/attest
docs](https://github.com/actions/attest#container-image)

**Update `anchore/sbom-action`** from v0.18.0 to v0.24.0 (node24
support, ahead of the [June 2
deadline](https://github.blog/changelog/2025-09-19-deprecation-of-node-20-on-github-actions-runners/)).

All changes remain non-blocking for the release process
(`continue-on-error: true` preserved).

> 🤖 This PR was created with the help of Coder Agents, and is reviewed
by a human.
2026-03-30 13:15:13 +05:00
Ethan 13dfc9a9bb test: harden chatd relay test setup (#23759)
These chatd relay tests were seeding chats through
`subscriber.CreateChat(...)`, which wakes the subscriber and can race
local acquisition against the intended remote-worker setup.

Seed waiting and remote-running chats directly in the database instead,
and point the default OpenAI provider at a local safety-net server so
accidental processing fails locally instead of reaching the live API.

Closes https://github.com/coder/internal/issues/1430
2026-03-30 17:52:01 +11:00
Ethan 54738e9e14 test(coderd/x/chatd): avoid zero-ttl config cache flake (#23762)
This fixes a flaky `TestConfigCache_UserPrompt_ExpiredEntryRefetches` by
making the seeded user prompt entry unambiguously expired before the
cache lookup runs.

The test previously inserted a `tlru` entry with a zero TTL, which
depends on `Set` and `Get` landing in different clock ticks. Switching
that seed entry to a negative TTL keeps the bounded `tlru` cache
behavior while removing the same-tick race.

Close https://github.com/coder/internal/issues/1432
2026-03-30 17:51:51 +11:00
TJandJaayden Halko 78986efed8 fix(site): hide table headers during loading and empty states on workspaces page (#23446)
## Problem

When the workspaces table transitions between states (loading →
populated, or populated → empty search results), the table column
headers visibly jump. This happens because the Actions column's width is
content-driven: when workspace rows are present, the action buttons give
it intrinsic width, shrinking the Name/Template/Status columns. When the
body is empty or loading, the Actions column collapses to zero, and the
other columns expand to fill the space.

## Solution

Hide the header content during loading and empty states using
`visibility: hidden` (Tailwind's `invisible` class), which preserves the
row's layout height but hides the text. This prevents the visual jump
since headers aren't visible during the states where column widths
differ.

- **Loading**: first column shows a skeleton bar matching the body
skeleton aesthetic; other columns are invisible
- **Empty search results**: all header content is invisible
- **Populated**: headers display normally

---------

Co-authored-by: Jaayden Halko <jaayden@coder.com>
2026-03-29 23:28:30 -07:00
Kyle Carberry 4d2b0a2f82 feat: persist skills as message parts like AGENTS.md (#23748)
## Summary

Skills are now discovered once on the first turn (or when the workspace
agent changes) and persisted as `skill` message parts alongside
`context-file` parts. On subsequent turns, the skill index is
reconstructed from persisted parts instead of re-dialing the workspace
agent.

This makes skills consistent with the AGENTS.md pattern and is
groundwork for a future `/context` endpoint that surfaces loaded
workspace context to the frontend.

## Changes

- Add `skill` `ChatMessagePartType` with `SkillName` and
`SkillDescription` fields
- Extend `persistInstructionFiles` to also discover and persist skills
as parts
- Add `skillsFromParts()` to reconstruct skill index from persisted
parts on subsequent turns
- Update `runChat()` to use `skillsFromParts` instead of re-dialing
workspace for skills
- Frontend: handle new `skill` part type (skip rendering, hide
metadata-only messages)

## Before / After

| | AGENTS.md | Skills |
|---|---|---|
| **Before** | Persist as `context-file` parts, reconstruct from parts |
In-memory `skillsCache` only, re-dial workspace on cache miss |
| **After** | Persist as `context-file` parts, reconstruct from parts |
Persist as `skill` parts, reconstruct from parts |

The in-memory `skillsCache` remains for `read_skill`/`read_skill_file`
tool calls that need full skill bodies on demand.

<details><summary>Design context</summary>

This is the first step toward a unified workspace context
representation. Currently:
- Context files are persisted as message parts (works)
- Skills were only in-memory (inconsistent)
- Workspace MCP servers are cached in-memory (future work)

Persisting skills as parts means a future `/context` endpoint can query
both context files and skills from the same message parts in the DB,
without depending on ephemeral server-side caches.
</details>
2026-03-29 21:48:17 -04:00
Ethan f7aa46c4ba fix(scaletest/llmmock): emit Anthropic SSE event lines (#23587)
The llmmock Anthropic stream wrote each chunk as `data:` only, so
Anthropic clients never saw the named SSE events they dispatch on and
Claude responses arrived empty even though the stream completed with
HTTP 200.

Update `sendAnthropicStream()` to emit `event: <type>` and `data:
<json>` for each Anthropic chunk while leaving the OpenAI-style streams
unchanged.
2026-03-30 12:21:53 +11:00
dependabot[bot] 4bf46c4435 chore: bump the coder-modules group across 2 directories with 1 update (#23757)
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>
2026-03-30 00:37:51 +00:00
Kyle Carberry be99b3cb74 fix: prioritize context cancellation in WebSocket sendEvent (#23756)
## Problem

Commit 386b449 (PR #23745) changed the `OneWayWebSocketEventSender`
event channel from unbuffered to buffered(64) to reduce chat streaming
latency. This introduced a nondeterministic race in `sendEvent`:

```go
sendEvent := func(event codersdk.ServerSentEvent) error {
    select {
    case eventC <- event:  // buffered channel — almost always ready
    case <-ctx.Done():     // also ready after cancellation
    }
    return nil
}
```

After context cancellation, Go's `select` randomly picks between two
ready cases, so `send()` sometimes returns `nil` instead of `ctx.Err()`.
With the old unbuffered channel the send case was rarely ready (no
reader), masking the bug.

## Fix

Add a priority `select` that checks `ctx.Done()` before attempting the
channel send:

```go
select {
case <-ctx.Done():
    return ctx.Err()
default:
}
select {
case eventC <- event:
case <-ctx.Done():
    return ctx.Err()
}
```

This is the standard Go pattern for prioritizing one channel over
another. When the context is already cancelled, the first select returns
immediately. The second select still handles the case where cancellation
happens concurrently with the send.

## Verification

- Ran the flaky test 20× in a loop (`-count=20`): all passed
- Ran the full `TestOneWayWebSocketEventSender` suite 5× (`-count=5`):
all passed
- Ran the complete `coderd/httpapi` test package: all passed

Fixes coder/internal#1429
2026-03-29 20:11:30 -04:00
Danielle Maywood 588beb0a03 fix(site): restore mobile back button on agent settings pages (#23752) 2026-03-28 23:12:49 +00:00
Michael Suchacz bfeb91d9cd fix: scope title regeneration per chat (#23729)
Previously, generating a new agent title used a page-global pending
state, so one in-flight regeneration disabled the action for every chat
in the Agents UI.

This change tracks regenerations by chat ID, updates the Agents page
contracts to use `regeneratingTitleChatIds`, and adds sidebar story
coverage that proves only the active chat is disabled.
2026-03-29 00:01:53 +01:00
Danielle Maywood a399aa8c0c refactor(site): restructure AgentsPage folder (#23648) 2026-03-28 21:33:42 +00:00
Kyle Carberry 386b449273 perf(coderd): reduce chat streaming latency with event-driven acquisition (#23745)
Previously, when a user sent a message, there was a 0–1000ms (avg
~500ms) polling delay before processing began.
`SendMessage`/`CreateChat`/`EditMessage` set `status='pending'` in the
DB and returned, but nothing woke the processing loop — it was a blind
1-second ticker.

## Changes

**Event-driven acquisition (main change):** Adds a `wakeCh` channel to
the chatd `Server`. `CreateChat`, `SendMessage`, `EditMessage`, and
`PromoteQueued` call `signalWake()` after committing their transactions,
which wakes the run loop to call `processOnce` immediately. The 1-second
ticker remains as a fallback safety net for edge cases (stale recovery,
missed signals).

**Buffer WebSocket write channel:** Changes the
`OneWayWebSocketEventSender` event channel from unbuffered to buffered
(64), decoupling the event producer from WebSocket write speed. The
existing 10s write timeout guards against stuck connections.

<details><summary>Implementation plan & analysis</summary>

The full latency analysis identified these sources of delay in the
streaming pipeline:

1. **Chat acquisition polling** — 0–1000ms (avg 500ms) dead time per
message. Fixed by wake channel.
2. **Unbuffered WebSocket write channel** — each token blocked on the
previous WS write completing. Fixed by buffering.
3. **PersistStep DB transaction per step** — `FOR UPDATE` lock + batch
insert. Not addressed in this PR (medium risk, would overlap DB write
with next provider TTFB).
4. **Multi-hop channel pipeline** — 4 channel hops per token. Not
addressed (medium complexity).

</details>

<details><summary>Test stabilization notes</summary>

`signalWake()` causes the chatd daemon to process chats immediately
after creation/send/edit, which exposed timing assumptions in several
tests that expected chats to remain in `pending` status long enough to
assert on. These tests were updated with `require.Eventually` +
`WaitUntilIdleForTest` patterns to wait for processing to settle before
asserting.

The race detector (`test-go-race-pg`) shows failures in
`TestCreateWorkspaceTool_EndToEnd` and `TestAwaitSubagentCompletion` —
these appear to be pre-existing races in the end-to-end chat flow that
are now exercised more aggressively because processing starts
immediately instead of after a 1s delay. Main branch CI (race detector)
passes without these changes.

</details>
2026-03-28 15:26:42 -04:00
Danielle Maywood 565cf846de fix(site): fix exec tool layout shift on command expand (#23739) 2026-03-28 16:27:54 +00:00
Kyle Carberry a2799560eb fix: use Popover for context indicator on mobile viewports (#23747)
## Problem

The context-usage indicator ring in the agents chat uses a Radix UI
`Tooltip`, which only opens on hover. On mobile/touch devices there is
no hover event, so tapping the indicator does nothing.

## Fix

On mobile viewports (`< 640 px`, matching the existing
`isMobileViewport()` helper), render a `Popover` instead of a `Tooltip`
so that tapping the ring toggles the context-usage info. Desktop
behavior (hover tooltip) is unchanged.

- Extract the trigger button and content into shared variables to avoid
duplication
- Conditionally render `Popover` (mobile) or `Tooltip` (desktop) based
on viewport width
- Both `Popover` and `PopoverContent` were already imported in the file
2026-03-28 12:26:23 -04:00
Michael Suchacz 73bde99495 fix(site): prevent mobile scroll jump during active touch gestures (#23734) 2026-03-28 16:24:12 +01:00
Kyle Carberry a708e9d869 feat: add tool rendering for read_skill, read_skill_file, start_workspace (#23744)
These tools previously fell through to the `GenericToolRenderer` which
showed a wrench icon and the raw tool name. Now they get dedicated icons
and contextual labels.

## Changes

**ToolIcon.tsx** — new icon mappings:
- `read_skill` / `read_skill_file` → `BookOpenIcon`
- `start_workspace` → `PlayIcon`
- `web_search` → `SearchIcon`

**ToolLabel.tsx** — contextual labels:
- `read_skill`: "Reading skill {name}…" → "Read skill {name}"
- `read_skill_file`: "Reading {skill}/{path}…" → "Read {skill}/{path}"
- `start_workspace`: "Starting workspace…" → "Started {name}"
- `web_search`: "Searching \"{query}\"…" → "Searched \"{query}\""

**tool.stories.tsx** — 12 new stories covering running, completed, and
error states for all four tools.

<details>
<summary>Visually verified in Storybook</summary>

All 10 story variants verified:
- ReadSkillRunning / ReadSkillCompleted / ReadSkillError
- ReadSkillFileRunning / ReadSkillFileCompleted / ReadSkillFileError
- StartWorkspaceRunning / StartWorkspaceCompleted / StartWorkspaceError
- WebSearchRunning / WebSearchCompleted / WebSearchNoQuery
</details>
2026-03-28 11:16:11 -04:00
Michael Suchacz 91217a97b9 fix(coderd/x/chatd): guard title generation meta replies (#23708)
Short prompts were producing title-generation meta responses such as "I
am a title generator" and prompt-echo titles. This rewrites the
automatic and manual title prompts to be shorter, less self-referential,
and more focused on returning only the title text.

The change also removes the broader post-generation guard layer, updates
manual regeneration to send real conversation text instead of a meta
instruction, and keeps regression coverage focused on the slimmer prompt
contract.
2026-03-28 15:58:53 +01:00
Danielle Maywood 399080e3bf fix(site/src/pages/AgentsPage): preserve right panel tab state across switches (#23737) 2026-03-28 00:00:17 +00:00
Danielle Maywood 50d9d510c5 refactor(site/src/pages/AgentsPage): clean up manual ref-sync callback patterns (#23732) 2026-03-27 23:09:51 +00:00
eda1bba969 fix(site): show table loader during session list pagination (#23719)
This patch shows the loading spinner on the AI Bridge Sessions table
during pagination. These API calls can take a noticeable amount of time
on dogfood, and without this it shows stale data while the fetch is
fetching.

Claude with the assist 🤖

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Kayla はな <mckayla@hey.com>
2026-03-27 19:09:42 -04:00
Danielle MaywoodandKayla はな 808dd64ef6 fix(site): reduce dropdown menu font size on agents page (#23711)
Co-authored-by: Kayla はな <mckayla@hey.com>
2026-03-27 23:09:27 +00:00
Kayla はな 04f7d19645 chore: additional typescript import modernization (#23722) 2026-03-27 16:41:56 -06:00
71a492a374 feat: implement <ClientFilter /> to AI Bridge request logs (#22694)
Closes #22136

This pull-request implements a `<ClientFilter />` to our `Request Logs`
page for AI Bridge. This will allow the user to select a client which
they wish to filter against. Technically the backend is able to actually
filter against multiple clients at once however the frontend doesn't
currently have a nice way of supporting this (future improvement).

<img width="1447" height="831" alt="image"
src="https://github.com/user-attachments/assets/0be234e2-25f2-4a89-b971-d74817395da1"
/>

---------

Co-authored-by: Jeremy Ruppel <jeremy.ruppel@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 17:18:28 -04:00
Charlie VoiselleandKayla はな 8c494e2a77 fix(site): sever opener in openAppInNewWindow for slim-window apps (#23117)
Split out from #23000.

## Problem

`openAppInNewWindow()` opens workspace apps in a slim popup without
`noopener`, leaving `window.opener` intact. Since workspace apps can
proxy arbitrary user-hosted content under the dashboard origin, this
exposes the Coder dashboard to tabnabbing and same-origin DOM access.

We cannot simply pass `"noopener"` to `window.open()` because the WHATWG
spec mandates that `window.open()` returns `null` when `"noopener"` is
present — indistinguishable from a blocked popup — which would break the
popup-blocked error toast.

## Solution

Use a two-step approach in `openAppInNewWindow()`:

1. Open `about:blank` first (without `noopener`) so we can detect popup
blockers via the `null` return
2. If the popup succeeded, sever the opener reference with `popup.opener
= null`
3. Navigate the popup to the target URL via `popup.location.href`

This preserves popup-block detection while eliminating the security
exposure.

## Tests

A vitest case in `AppLink.test.tsx` asserts that `open_in="slim-window"`
anchors do not carry `target` or `rel` attributes (since slim-window
opening is handled programmatically via `onClick` / `window.open()`, not
anchor attributes).

---------

Co-authored-by: Kayla はな <kayla@tree.camp>
2026-03-27 15:35:10 -04:00
Kyle Carberry 839165818b feat(coderd/x/chatd): add skills discovery and tools for chatd (#23715)
Adds skill discovery and tools to chatd so the agent can discover and
load `.agents/skills/` from workspaces, following the same pattern as
AGENTS.md instruction loading and MCP tool discovery.

## What changed

### `chattool/skill.go` — discovery, loading, and tools

- **DiscoverSkills** — walks `.agents/skills/` via `conn.LS()` +
`conn.ReadFile()`, parses SKILL.md frontmatter (name + description),
validates kebab-case names match directory names, silently skips
broken/missing entries.
- **FormatSkillIndex** — renders a compact `<available-skills>` XML
block for system prompt injection (~60 tokens for 3 skills). Progressive
disclosure: only names + descriptions in context, full body loaded on
demand.
- **LoadSkillBody** / **LoadSkillFile** — on-demand loading with path
traversal protection and size caps (64KB for SKILL.md, 512KB for
supporting files).
- **read_skill** / **read_skill_file** tools — `fantasy.AgentTool`
implementations following the same pattern as ReadFile and
WorkspaceMCPTool. Receive pre-discovered `[]SkillMeta` via closure to
avoid re-scanning on every call.

### `chatd.go` — integration into runChat

- Skills discovered in the `g2` errgroup parallel with instructions and
MCP tools.
- `skillsCache` (sync.Map) per chat+agent, same invalidation pattern as
MCP tools cache.
- Skill index injected via `InsertSystem` after workspace instructions.
- Re-injected in `ReloadMessages` callback so it survives compaction.
- `read_skill` + `read_skill_file` tools registered when skills are
present (for both root and subagent chats).
- Cache cleaned up in `cleanupStreamIfIdle` alongside MCP tools cache.

## Format compatibility

Uses the same `.agents/skills/<name>/SKILL.md` format as
[coder/mux](https://github.com/coder/mux) and
[openai/codex](https://github.com/openai/codex).
2026-03-27 15:22:13 -04:00
Kyle Carberry 6b77fa74a1 fix: remove bold font-weight from unread chats in agents sidebar (#23725)
Removes the `font-semibold` class applied to unread chat titles in the
`/agents` sidebar. The unread indicator dot already provides sufficient
visual distinction for unread chats.
2026-03-27 13:36:49 -04:00
Atif AliandClaude Sonnet 4.6 25e9fa7120 ci: improve Linear release tracking for scheduled pipeline (#23357)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 22:26:02 +05:00
Danielle Maywood 60065f6c08 fix(site): replace inline delete confirmations with modals (#23710) 2026-03-27 17:02:18 +00:00
Kyle Carberry bcdc35ee3e feat: add chat read/unread indicator to sidebar (#23129)
## Summary

Adds read/unread tracking for chats so users can see which agent
conversations have new assistant messages they haven't viewed.

## Backend Changes

- Adds `last_read_message_id` column to the `chats` table (migration
000439).
- Computes `has_unread` as a virtual column in `GetChatsByOwnerID` using
an `EXISTS` subquery checking for assistant messages beyond the read
cursor.
- Exposes `has_unread` on the `codersdk.Chat` struct and auto-generated
TypeScript types.
- Updates `last_read_message_id` on stream connect/disconnect in
`streamChat`, avoiding per-message API calls during active streaming.
- Uses `context.WithoutCancel` for the deferred disconnect write so the
DB update succeeds even after the client disconnects.

## Frontend Changes

- Bold title (`font-semibold`) for unread chats in the sidebar.
- Small blue dot indicator next to the relative timestamp.
- Suppresses unread indicator for the currently active chat via
`isActive` from NavLink.

## Design Decisions

- Only `assistant` messages count as unread — the user's own messages
don't trigger the indicator.
- No foreign key on `last_read_message_id` since messages can be deleted
(via rollback/truncation) and the column is just a high-water mark.
- Zero API calls during streaming: exactly 2 DB writes per stream
session (connect + disconnect).
- Unread state refreshes on chat list load and window focus. The
`watchChats` WebSocket optimistically marks non-active chats as unread
on `status_change` events, but does not carry a server-computed
`has_unread` field. Navigating to a chat optimistically clears its
unread indicator in the cache.
2026-03-27 12:15:04 -04:00
Cian Johnston a5c72ba396 fix(coderd/agentapi): trim whitespace from workspace agent metadata values (#23709)
- Trim leading/trailing whitespace from metadata `value` and `error`
fields before storage
- Trimming happens before length validation so whitespace-padded values
are handled correctly
- Add `TrimWhitespace` test covering spaces, tabs, newlines, and
preserved inner whitespace
- No backfill needed (unlogged table, stores only latest value)

> 🤖 Created by a Coder Agent, reviewed by me.
2026-03-27 15:08:47 +00:00
Cian Johnston 3f55b35f68 refactor: replace AsSystemRestricted with narrower actors (#23712)
Replace overly-broad `AsSystemRestricted` with purpose-built actors:

- **OAuth2 provider paths** → `AsSystemOAuth2` (13 call sites across
`tokens.go`, `registration.go`, `apikey.go`)
- **Provisioner daemon health read** → `AsSystemReadProvisionerDaemons`
(1 site in `healthcheck/provisioner.go`)
- **Provisionerd file cache paths** → `AsProvisionerd` (2 sites in
`provisionerdserver.go`, matching existing usage nearby)

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

Each replacement actor is a strict subset of `AsSystemRestricted`. Every
DB method
at each call site is already covered by the narrower actor's
permissions:

- `subjectSystemOAuth2`: OAuth2App/Secret/CodeToken (all), ApiKey (Read,
Delete), User (Read), Organization (Read)
- `subjectSystemReadProvisionerDaemons`: ProvisionerDaemon (Read)
- `subjectProvisionerd`: File (Create, Read) plus provisionerd-scoped
resources

No new permissions added. `nolint:gocritic` comments updated to reflect
the new actors.
</details>

> 🤖 Created by a Coder Agent, reviewed by me.
2026-03-27 15:08:30 +00:00
Ehab Younes 97a27d3c09 fix(site/src/pages/AgentsPage): fire chat-ready after store sync so scroll-to-bottom hits rendered DOM (#23693)
`onChatReady` now waits for the store to have all fetched messages (not
just query success), so the DOM has content when the parent frame acts
on the signal. Removed the `scroll-to-bottom` round-trip from
`onChatReady`, the embed page no longer needs the parent to echo a
scroll command back.

Also moved `autoScrollRef` check before the `widthChanged` guard in
`ScrollAnchoredContainer`'s ResizeObserver. The width guard is only
relevant for scroll compensation (user scrolled up); it should never
prevent pinning to bottom when auto-scroll is active. Also tightened the
store sync guard to `storeMessageCount < fetchedMessageCount`.
2026-03-27 18:01:38 +03:00
Kyle Carberry 4ed9094305 perf(site): memoize chat rendering hot path (#23720)
Addresses chat page rendering performance. Profiling with React Profiler
showed `AgentChat` actual render times of 20–31ms (exceeding the
16ms/60fps
budget), with `StickyUserMessage` as the #1 component bottleneck at
35.7%
of self time.

## Changes

**Hoist `createComponents` to module scope** (`response.tsx`):
Previously every `<Response>` instance called `createComponents()` per
render, creating a fresh components map that forced Streamdown to
discard
its cached render tree. Now both light/dark variants are precomputed
once
at module scope.

**Wrap `StickyUserMessage` in `memo()`** (`ConversationTimeline.tsx`):
Profile-confirmed #1 bottleneck. Each instance carries
IntersectionObserver
+ ResizeObserver + scroll handlers; skipping re-render avoids all that
setup.

**Wrap `ConversationTimeline` in `memo()`**
(`ConversationTimeline.tsx`):
Prevents cascade re-renders from the parent when props haven't changed.

**Remove duplicate `buildSubagentTitles`** (`ConversationTimeline.tsx` →
`AgentDetailContent.tsx`): Was computed in both `AgentDetailTimeline`
and
`ConversationTimeline`. Now computed once and passed as a prop.

<details>
<summary>Profiling data & analysis</summary>

### Profiler Metrics
| Metric | Value |
|--------|-------|
| INP (Interaction to Next Paint) | 82ms |
| Processing duration (event handlers) | 52ms |
| AgentChat actual render | 20–31ms (budget: 16ms) |
| AgentChat base render (no memo) | ~100ms |

### Top Bottleneck Components (self-time %)
| Component | Self Time | % |
|-----------|-----------|---|
| StickyUserMessage | 11.0ms | 35.7% |
| ForwardRef (radix-ui) | 7.4ms | 24.0% |
| Presence (radix-ui) | 2.0ms | 6.5% |
| AgentChatInput | 1.4ms | 4.5% |

### Decision log
- Chose module-scope precomputation over `useMemo` for
`createComponents`
  because there are only two possible theme variants and they're static.
- Did not add virtualization — sticky user messages + scroll anchoring
  make it complex. The memoization fixes should be measured first.
- Did not wrap `BlockList` in `memo()` — the React Compiler (enabled for
  `pages/AgentsPage/`) already auto-memoizes JSX elements inside it.
- Phase 2 (verify React Compiler effectiveness on
`parseMessagesWithMergedTools`)
and Phase 3 (radix-ui Tooltip lazy-mounting) deferred to follow-up PRs.

</details>
2026-03-27 14:28:27 +00:00
Kyle Carberry d973a709df feat: add model_intent option to MCP server configs (#23717)
Add a per-MCP-server `model_intent` toggle that wraps tool schemas with
a
`model_intent` field, requiring the LLM to provide a human-readable
description of each tool call's purpose. The intent string is shown as a
status label in the UI instead of opaque tool names, and is
transparently
stripped before the call reaches the remote MCP server.

Built-in tools have rich specialized renderers (terminal blocks, file
diffs,
etc.) and don't need this. MCP tools hit `GenericToolRenderer` which
only
shows raw tool names and JSON — that's where model_intent adds value.

The model learns what to provide via the JSON Schema `description` on
the
`model_intent` property itself — no system prompt changes needed.

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

### Architecture

Inspired by the `withModelIntent()` pattern from `coder/blink`, adapted
for
Go + React. The wrapping is entirely in the `mcpclient` layer — tool
implementations never see `model_intent`.

**Schema wrapping** (`mcpToolWrapper.Info()`): When enabled, wraps the
original tool parameters under a `properties` key and adds a
`model_intent`
string field with a rich description that teaches the model inline.

**Input unwrapping** (`mcpToolWrapper.Run()`): Strips `model_intent` and
unwraps `properties` before forwarding to the remote MCP server. Handles
three input shapes models may produce:
1. `{ model_intent, properties: {...} }` — correct format
2. `{ model_intent, key: val, ... }` — flat, no wrapper
3. Malformed — falls through gracefully

**Frontend extraction**: `streamState.ts` extracts `model_intent` from
incrementally parsed streaming JSON. `messageParsing.ts` extracts it
from
persisted tool call args.

**UI rendering**: `GenericToolRenderer` shows the capitalized intent
string
as the primary label when available, falling back to the raw tool name.

### Changes
- Database: `model_intent` boolean column on `mcp_server_configs`
- SDK: `ModelIntent` field on config/create/update types
- API: pass-through in create/update handlers + converter
- mcpclient: schema wrapping in `Info()`, input unwrapping in `Run()`
- Frontend: extraction from streaming + persisted args
- UI: intent label in `GenericToolRenderer`, toggle in admin panel
- Tests: 6 new tests (schema wrapping, unwrapping, passthrough,
fallback)

### Decision log
- **Option lives on MCPServerConfig, not model config**: Built-in tools
  already have rich renderers; only MCP tools benefit from model_intent.
- **No system prompt changes**: The JSON Schema `description` on the
  `model_intent` property teaches the model inline.
- **Pointer bool on update request**: Follows existing pattern (`*bool`)
  so PATCH requests don't reset the value when omitted.

</details>
2026-03-27 14:23:25 +00:00
Kyle Carberry 50c0c89503 fix(coderd): refresh expired MCP OAuth2 tokens everywhere (#23713)
Fixes expired MCP OAuth2 tokens causing 401 errors and stale
`auth_connected` status in the UI.

When users authenticate MCP servers (e.g. GitHub) via OAuth2, the access
token and refresh token are stored in the database. However, when the
access token expired, nothing refreshed it anywhere:

- **chatd**: sent the expired token as-is, getting a 401 and skipping
the MCP server
- **list/get endpoints**: reported `auth_connected: true` just because a
token record existed, regardless of expiry

## Changes

### Shared utility: `mcpclient.RefreshOAuth2Token`

Pure function that uses `golang.org/x/oauth2` `TokenSource` to check if
a token is expired (or within 10s of expiry) and refresh it. No DB
dependency — callers handle persistence.

### chatd (`coderd/x/chatd/chatd.go`)

Before calling `mcpclient.ConnectAll`, refreshes expired tokens.
Persists new credentials to the database. Falls back to the old token if
refresh fails.

### List/get MCP server endpoints (`coderd/mcp.go`)

Both `listMCPServerConfigs` and `getMCPServerConfig` now attempt refresh
when checking `auth_connected`. If the token is expired:
- **Has refresh token**: attempt refresh, persist result, report
`auth_connected` based on success
- **No refresh token**: report `auth_connected: false` if expired

This means the UI accurately reflects whether the user's token is
actually usable, rather than just whether a record exists.

<details>
<summary>Design notes</summary>

- `RefreshOAuth2Token` lives in `mcpclient` to avoid circular imports
(`coderd` → `chatd` → `mcpclient` is fine; `chatd` → `coderd` would be
circular).
- DB persistence is handled by each caller with their own authz context
(`AsSystemRestricted` in both cases).
- The `buildAuthHeaders` warning in mcpclient about expired tokens is
kept as defense-in-depth logging.
</details>
2026-03-27 10:06:32 -04:00
Danielle Maywood 0ec0f8faaf fix: guard per-chat cancelQueries to prevent 'Chat not found' race (#23714) 2026-03-27 13:08:58 +00:00
Spike Curtis 9b4d15db9b chore: add Tunneler FSM and partial impl (#23691)
<!--

If you have used AI to produce some or all of this PR, please ensure you
have read our [AI Contribution
guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING)
before submitting.

-->

Adds the Tunneler state machine and logic for handling build updates.   
  
This is a partial implementation and tests. Further PRs will fill out
the other event types.
  
Relates to GRU-18
2026-03-27 08:52:13 -04:00
Matt Vollmer 9e33035631 fix: optimistic pinned reorder and post-drag click suppression (#23704)
## Summary

Fixes two issues with pinned chat drag-to-reorder in the agents sidebar:

1. **Missing optimistic reorder**: After dragging a pinned chat to a new
position, the sidebar flashed back to the old order while waiting for
the server response, then snapped to the new order. Now the react-query
cache is updated optimistically in `onMutate` so the reorder is visually
instant.

2. **Post-drag navigation on upward drag**: Dragging a pinned chat
**upward** caused unintended navigation to that chat on drop. The
browser synthesizes a click from the final `pointerup`, and the previous
suppression listener was attached too low in the DOM tree to reliably
intercept it after items reorder.

## Changes

### Optimistic cache reorder (`site/src/api/queries/chats.ts`)

`reorderPinnedChat.onMutate` now extracts all pinned chats from the
cache, reorders them to match the new position, and writes the updated
`pin_order` values back via `updateInfiniteChatsCache`. The server
response still reconciles on `onSettled`.

### Document-level click suppression (`AgentsSidebar.tsx`)

- Moved click suppression from the pinned container div to
`document.addEventListener('click', handler, true)`. This fires before
any element-level handlers regardless of DOM reordering during drag.
Scoped via `pinnedContainerRef.contains(target)` so it only affects
pinned rows.
- Replaced `recentDragRef` (boolean cleared by `setTimeout(100ms)`) with
`lastDragEndedAtRef` (`performance.now()` timestamp checked against a
300ms window). This eliminates the race where the timeout fires before
the synthetic click arrives.

---

PR generated with Coder Agents
2026-03-27 07:57:23 -04:00
Ethan 83b2f85d63 feat(site/src/pages/AgentsPage): add ArrowUp shortcut to edit last user message (#23705)
Add a keyboard shortcut (ArrowUp on empty input) to start editing the
most recent user message, mirroring Mux's behavior. The shortcut reuses
the existing history-edit flow triggered by the pencil button.

Extract a shared `getEditableUserMessagePayload` helper so the pencil
button and the new shortcut both derive the edit payload identically.
Derive the last editable user message during render in
`AgentDetailInput` from the existing store selectors, keeping the
implementation Effect-free and React Compiler friendly.
2026-03-27 21:43:31 +11:00
Ethan c4ef94aacf fix(coderd/x/chatd): prevent chat hang when workspace agent is unavailable (#23707)
## Problem

Chats with a persisted `agent_id` binding hang indefinitely when the
workspace is stopped. The stale agent row still exists in the DB, so
`ensureWorkspaceAgent` succeeds, but the dial blocks forever in
`AwaitReachable`. The MCP discovery goroutine used an unbounded context,
so `g2.Wait()` never returned and the LLM never started.

## Fix

Three targeted changes restore the pre-binding behavior where stopped
workspaces degrade gracefully instead of blocking:

1. **`dialWithLazyValidation`**: "no agents in latest build" is now a
terminal fast-fail — the hanging dial is canceled and
`errChatHasNoWorkspaceAgent` returned immediately, instead of falling
through to `waitForOriginalDial`.

2. **Pre-LLM workspace setup**: MCP discovery and instruction
persistence gate on `workspaceAgentIDForConn` before attempting any
dial. MCP discovery is bounded by a 5s timeout and checks the in-memory
tool cache first (using the cheap cached agent from
`ensureWorkspaceAgent`), so the common subsequent-turn path has zero DB
queries.

3. **`persistInstructionFiles`**: tracks whether the workspace
connection succeeded and skips sentinel persistence on failure, so the
next turn retries if the workspace is restarted.

## Scenarios

**Running workspace, subsequent turn (hot path):** MCP cache hit via
in-memory cached agent. Zero DB queries, zero dials. Unchanged from
#23274.

**Stopped workspace, persisted binding (the bug):** MCP cache hit (stale
descriptors, fine — they fail at invocation). Pre-LLM setup completes
instantly. Tool invocation enters `dialWithLazyValidation`, dial fails
or hangs, validation discovers no agents, returns
`errChatHasNoWorkspaceAgent`. Model sees the error and can call
`start_workspace`.

**New chat, running workspace:** `ensureWorkspaceAgent` resolves via
latest-build, persists binding. MCP discovery dials and caches tools.

**New chat, stopped workspace:** `ensureWorkspaceAgent` finds no agents,
returns `errChatHasNoWorkspaceAgent`. Pre-LLM setup skips. LLM starts
with built-in tools only.

**Rebuilt workspace (agent switched):** MCP cache hit with stale agent
(harmless for one turn). Tool invocation dials stale agent, fails fast,
`dialWithLazyValidation` switches to new agent, persists updated
binding.

**Workspace restarted after stop:** No sentinel was persisted during the
stopped turn, so instruction persistence retries. Agent binding switches
to the new agent via `workspaceAgentIDForConn`.

**Transient DB error during validation:** Not
`errChatHasNoWorkspaceAgent`, so `dialWithLazyValidation` falls through
to `waitForOriginalDial` (cannot prove stale). No false positive.

**Tool invocation on stopped workspace:** `getWorkspaceConn` calls
`ensureWorkspaceAgent` (returns stale row), then
`dialWithLazyValidation` validation discovers no agents, returns
`errChatHasNoWorkspaceAgent`, cached state cleared, error returned to
model.
2026-03-27 18:47:39 +11:00
Ethan d678c6fb16 fix(coderd/x/chatd): forward local status events to fix delayed-startup banner (#23650)
## Problem

The agent chat delayed-startup banner ("Response startup is taking
longer than expected") could appear even though the model was already
streaming.

The root cause is in `Subscribe()`: `message_part` events were delivered
via the fast local in-process stream, while `status` events were
delivered via PostgreSQL pubsub. Both feed into the same `select`
statement, and Go's `select` picks whichever channel is ready first —
there is no ordering guarantee between channels. So a `message_part`
could outrun the `status=running` that logically precedes it.

The frontend saw content arrive while it still thought the chat was
pending, triggering the banner.

## Fix

Also forward `status` events from the local channel, alongside
`message_part`.

Both event types already travel through the same FIFO subscriber
channel: `publishStatus()` is called before the first `message_part`, so
channel ordering guarantees the frontend sees `status=running` before
any content.

Pubsub still delivers a duplicate `status` event later; the frontend
deduplicates it (`setChatStatus` is idempotent — it early-returns when
the status hasn't changed).
2026-03-27 17:55:19 +11:00