> Mux updated this PR on behalf of Mike.
## Stack Context
This PR is the storage, permissions, API, and SDK layer for experimental
personal skills. #25362 has landed on `main`, so this branch is
restacked directly on `main`.
Stack order:
1. #25363 storage, permissions, API, and SDK
2. #25365 API test coverage
3. #25366 chattool and chatd integration
4. #25066 settings UI and docs
5. #25386 personal skills slash menu
## What?
Adds the `user_skills` database table, generated queries, RBAC resources
and scopes, audit resource handling, experimental user-scoped CRUD
endpoints, SDK types, and generated API/site types.
Follow-up review and restack fixes:
- Enforce a bounded personal skill description in parser and database
constraints.
- Return `403 Forbidden` for unauthorized create and update attempts.
- Return explicit conflict responses when soft-deleted users are
targeted.
- Keep user admins out of personal skills, while site owners can read
and delete but not create or update.
- Document trigger-raised constraint names and keep schema constants
covered by tests.
- Reuse `UserSkillMetadata` in the full `UserSkill` SDK response type.
- Generate user skill IDs in Go instead of relying on a database
default.
- Rebase on latest `main` and renumber the user skills migration to
`000502_user_skills`.
## Why?
Personal skills need durable user-owned storage with owner
authorization, limited site-owner moderation, and a hidden API surface
before chatd can consume them.
## Validation
- `make gen`
- `go test ./coderd/database -run '^TestUserSkillSchemaConstants$'
-count=1`
- `go test ./coderd/database/dbauthz -run
'^TestMethodTestSuite/TestUserSkills$' -count=1`
- `go test ./coderd -run '^TestPatchUserSkill$' -count=1`
- `go test ./codersdk ./coderd/database/db2sdk`
- `make lint`
- pre-commit hook on `97fd58108d`
The Authentication and BYOK docs are now part of their own section above
the Clients subsection. The original PR, coder/coder#25459, was based on
a ticket I generated to calculate the drift, but the contents of the
Linear ticket were geared more toward documenting _everything_ in the
code, which had too much scope and was confusing.
Fixes DOCS-148
<!--
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.
-->
> Mux opened this PR on behalf of Mike.
Fixes CODAGT-451
Adds optional `model_intent` metadata to the built-in execute tool
schema so tool calls can carry a short user-facing intent label without
duplicating the command or duration.
The Agents UI now composes that intent with the existing execute command
and duration fields, displaying labels like `Checking repository state
using git fetch origin for 2.3s` while keeping the shell command visible
as the audit-relevant action.
Existing execute calls without an intent keep the previous `Ran
<command>` fallback label, so only intent-bearing calls get the new
composed label.
Add frontend API methods, mocks, and form helpers for user secrets CRUD. The new client methods cover list, get, create, update, and delete requests, including URL encoding for secret names used in route paths.
Add user secret form utilities for create and update payload construction, required create field checks, and structured API validation error mapping back to form fields. User secret name validation now lives in codersdk with tests, and coderd returns field-level validation errors for create, update, and uniqueness conflicts so the frontend can show backend-owned validation results consistently.
Advisor tool calls currently reject questions over 2000 runes, which can
leave the parent model retrying the same invalid call.
This documents the limit in the advisor tool schema and guidance, then
truncates oversized questions rune-safely before building the nested
advisor prompt.
> Mux working on behalf of Mike.
Anthropic replay can fail when stored history contains a
provider-executed tool call like `web_search` without the matching
provider-executed result. That orphaned call is incomplete
provider-internal state, so replaying it can make an otherwise usable
chat unreplayable even though there is no search result to preserve.
This fixes replay by dropping orphan provider-executed tool calls from
the model-visible prompt, preserving signed reasoning and the rest of
the assistant content, then revalidating before the request. We do not
synthesize tool results or drop reasoning. The database can retain the
historical artifact for inspection, while Anthropic only sees replayable
content.
This matches permissively licensed prior art. Vercel AI SDK
(Apache-2.0), used by mux, keeps incomplete tool state in UI/history but
omits it from model requests with `convertToModelMessages(..., {
ignoreIncompleteToolCalls: true })`. LangChain, LiteLLM, and OpenAI
Agents (MIT for the relevant open-source code) also preserve Anthropic
signed reasoning as opaque replay data. Coder applies that model-visible
replay boundary explicitly because our persisted history is already in
provider-message form.
This matches mux, is cleaner than the older idea around not persisting
the search query tool, and the model handles the repaired prompt fine.
Closes CODAGT-448
## Before
<img width="963" height="491" alt="image"
src="https://github.com/user-attachments/assets/a7788ebf-2728-4420-90cf-5e4f6905bdf7"
/>
## After
<img width="842" height="513" alt="image"
src="https://github.com/user-attachments/assets/ae39c262-7586-4e2d-b7db-1b639a7e8e15"
/>
http.DefaultTransport is shared with httptest.Server, which calls
CloseIdleConnections on it during Close. Parallel subtests sharing this
transport could see their in-flight requests broken with 'net/http:
HTTP/1.x transport connection broken: http: CloseIdleConnections
called'. Most visibly this flaked TestExternalAuthCallback/ValidateURL.
Lazily create a dedicated http.Client per OAuth2Config so its idle
connection pool is not affected by unrelated httptest.Server.Close
calls.
Generated with assistance from Coder Agents.
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>
Replace the original 13-person roster with the full 47-person list,
sorted alphabetically by first name.
cc @DanielleMaywood for review
> [!NOTE]
> This PR was authored by Coder Agents.
Fixes https://linear.app/codercom/issue/PLAT-224
The Validates subtest only checks that `Run()` returns a validation
error and never reads PTY output. We don't need it in this test, so
removing.
> 🤖 Generated by Coder Agents
`attach_file` was registered for plan-mode turns but never added to
`builtinPlanToolAllowed`, so the per-turn `ActiveTools` allowlist
filtered it out and calls failed with `Tool not active in this turn:
attach_file`. This was an omission rather than a deliberate block — the
tool (#24280) landed shortly after plan mode (#24236) and no subsequent
edit to the allowlist picked it up.
Add `attach_file` under the `isRootChat` case, matching how other
artifact-producing tools (`propose_plan`, `write_file`, `edit_files`)
are gated. The tool only reads from the workspace and writes to
chat-attachment storage, so it preserves plan mode's invariant of not
making implementation changes to the workspace. Subagents in plan mode
remain restricted to the minimal read-only surface.
Removes `SkipCacheProviders: true` from the `external-agent` and
`ai-task-app-id` subtests of `TestProvision` so they go through
`testutil.CacheTFProviders` like every other coder/coder-using test.
Today both bypass the local provider mirror and hit
`registry.terraform.io` on every CI run, which flakes on transient 5xx
responses. The flag was originally added in #19286 as a workaround for
an unreleased provider build; that workaround has been stale since
`coder/coder v2.10.0` shipped.
This won't fix the flake outright. The first run after merge and any
future test-name/file-content change are still cache misses that go to
the registry. But in steady state on `main` the Actions cache will be
hit and `terraform init` runs fully offline, which should drop registry
exposure from ~every run to the small handful of cache-miss cases.
Relates to https://github.com/coder/internal/issues/1353
Relates to https://github.com/coder/internal/issues/1193
Records reasoning start and end times on persisted reasoning
`ChatMessagePart`s so reasoning duration can be computed for stored
chats. Backend-only: no SSE changes and no frontend rendering ship in
this PR.
The `created_at` field on `ChatMessagePart` is extended to also be
present on `reasoning` parts (it previously appeared only on `tool-call`
and `tool-result`), and a new `completed_at` field is added for
`reasoning` parts.
### How timestamps are recorded
- `StreamPartTypeReasoningStart`: stamp `startedAt = dbtime.Now()` on
the active reasoning state.
- `StreamPartTypeReasoningEnd`: stamp `completedAt = dbtime.Now()` and
append both into parallel `[]time.Time` slices on `stepResult`.
- Persistence reads the slices in occurrence order (reasoning has no
provider-side ID) and applies them to the matching `ChatMessagePart` via
`buildAssistantPartsForPersist`. The first reasoning block's stamps go
onto the first reasoning part, and so on.
- `flushActiveState` flushes partial reasoning interrupted before
`StreamPartTypeReasoningEnd` with `startedAt` from the active state and
`completedAt = dbtime.Now()` at the interruption.
### Why two fields, not one?
Tool calls and results are point events. The frontend computes their
duration by subtracting the call's `created_at` from the result's
`created_at`. Reasoning is one assistant part that brackets a span, so
we record both endpoints on the part itself.
### Why not stamp in `PartFromContent`?
Same rationale as #24101: `PartFromContent` is called during both SSE
publishing and persistence. Stamping there would yield incorrect
persistence-time timestamps for reasoning blocks that finished much
earlier in the step. Instead we capture in the chatloop and apply during
persistence.
<details><summary>Implementation plan</summary>
- `codersdk/chats.go`: extend `CreatedAt`'s `variants` to include
`reasoning?`; add `CompletedAt *time.Time` with `variants:"reasoning?"`.
- `coderd/x/chatd/chatloop/chatloop.go`: extend `reasoningState` with
`startedAt`; extend `stepResult` and `PersistedStep` with parallel
`[]time.Time` reasoning slices; stamp on
`ReasoningStart`/`ReasoningEnd`; thread the slices through all
`PersistStep` call sites including the interrupt-safe path; record
partial reasoning in `flushActiveState`.
- `coderd/x/chatd/attachments.go`: walk reasoning parts in occurrence
order and apply `step.ReasoningStartedAt[i]` to `part.CreatedAt` and
`step.ReasoningCompletedAt[i]` to `part.CompletedAt`.
### Tests
- `codersdk/chats_test.go` round-trips `created_at` + `completed_at` on
reasoning parts and verifies omission when absent and partial
interrupted parts.
- `coderd/x/chatd/chatprompt/chatprompt_test.go` asserts
`PartFromContent(ReasoningContent{})` does NOT stamp timestamps.
- `coderd/x/chatd/chatloop/chatloop_test.go`
`TestRun_ReasoningTimestamps` drives a stream with two reasoning blocks
and verifies parallel slices, monotonicity, ordering, non-zero values,
and content-block ordering.
`TestRun_InterruptedReasoningFlushesTimestamps` cancels mid-reasoning
and verifies `flushActiveState` records a non-zero pair.
- `coderd/x/chatd/attachments_test.go` covers
`buildAssistantPartsForPersist` for normal interleaved reasoning,
partial (zero `completed_at`), and missing slices.
</details>
> Generated by Coder Agents.
Co-authored-by: Coder Agent <agent@coder.com>
## Problem
Mid-turn workspace MCP discovery was broken when an agent was still
cold-starting. `PrepareTools` in `chatd.go` flipped
`workspaceMCPDiscovered = true` *before* calling
`discoverWorkspaceMCPTools`, so a failed discovery attempt permanently
blocked retries within the turn.
Customer-reported repro:
- New chat with no pre-selected workspace.
- LLM calls `create_workspace` mid-turn at `23:35:05`.
- `PrepareTools` fires, dials the agent with a 30s timeout, dial times
out at `23:38:15`, `discoverWorkspaceMCPTools` returns empty.
- Agent connects at `23:38:29`, 14 seconds later.
- `workspaceMCPDiscovered` was already true, so `PrepareTools` never
retried for the rest of the turn. MCP tools only appeared on the next
user message.
A naive retry loop in `PrepareTools` would also miss the bigger picture:
a workspace boot can take several minutes (EC2 cold start, 10 min
startup scripts), and the chatloop only gets a chance to call
`PrepareTools` between LLM steps.
## Fix
Do the workspace MCP discovery from inside the tool that already waits
for the agent. `chattool.CreateWorkspace` and `chattool.StartWorkspace`
call `waitForAgentReady`, which has a 2 min agent-online budget plus a
10 min startup-script budget. By the time they fire `OnChatUpdated`, the
agent is `Ready`. The chatd `onChatUpdated` callback now launches an
async `primeWorkspaceMCPCache` goroutine on every bind that has a valid
workspace ID:
- The primer calls `discoverWorkspaceMCPTools` until it returns a
non-empty list or `workspaceMCPPrimeMaxWait` (30s) elapses, with a 2s
backoff between attempts. The bounded wait handles the short race
between agent-online and the agent's MCP `Connect` settling.
- The primer runs asynchronously so the tool itself never blocks. Some
templates simply do not advertise MCP tools, in which case the primer
would otherwise spend its full budget for nothing.
- The primer shares the chat `ctx` (not a detached one) so it is
canceled together with the chat. A dangling primer would re-dial the
workspace conn after `runChat`'s deferred `workspaceCtx.close()` and
leak that conn.
- `inflight.Add(1)` ensures server shutdown still waits for any
in-progress primer.
- `PrepareTools` is simplified back to a single discovery call. It now
only sets `workspaceMCPDiscovered = true` on success, so an empty result
no longer permanently blocks discovery within the turn. The cache hit
warmed by the primer makes that call cheap in the common case; the dial
fallback handles the rare cache miss.
## Tests
All in `coderd/x/chatd/chatd_internal_test.go`:
- `TestPrimeWorkspaceMCPCache_SuccessOnFirstAttempt` — single
`ListMCPTools` call returning tools populates the cache.
- `TestPrimeWorkspaceMCPCache_RetriesUntilToolsAppear` — first call
empty, second returns tools; primer retries past the backoff and writes
the cache. Uses `quartz.Mock.Trap` on `NewTimer`.
- `TestPrimeWorkspaceMCPCache_GivesUpAfterDeadline` — `ListMCPTools`
always empty; primer stops at `workspaceMCPPrimeMaxWait` and refuses to
cache the empty result so PrepareTools can retry on the next step.
The existing integration test
`TestRunChat_WorkspaceMCPDiscoveryAfterMidTurnCreateWorkspace` continues
to pass and now also exercises the async-primer path end-to-end via the
create_workspace tool.
```
go test ./coderd/x/chatd/... -count=1
go test ./coderd/x/chatd/ -race -count=1
make pre-commit
```
<details>
<summary>Design notes</summary>
- The first iteration of this PR added retry+cooldown+failure-cap logic
inside `PrepareTools`. It worked for the customer's ~30s race window but
did not help workspaces that take several minutes to boot, because
`PrepareTools` only fires between LLM steps. Reviewer pointed out the
right place to handle this is the tool itself; the current
implementation does that.
- Why async: a primer that ran synchronously inside the `OnChatUpdated`
callback blocked the create_workspace tool from returning for up to
`workspaceMCPPrimeMaxWait`, which broke
`TestCreateWorkspaceTool_EndToEnd` and would hurt any template that does
not expose MCP tools. Decoupling lets the tool return immediately and
lets the primer warm the cache concurrently with the next LLM step.
- Why share the chat `ctx` rather than `context.WithoutCancel(ctx)` (the
title-generation pattern): the primer touches
`workspaceCtx.getWorkspaceConn`, which `runChat`'s deferred
`workspaceCtx.close()` invalidates. A detached primer outliving the chat
would dial a fresh conn and leak it.
- The constant naming distinguishes `workspaceMCPDiscoveryTimeout` (35s
per-call dial budget, unchanged from #25169) from
`workspaceMCPPrimeMaxWait` (30s total budget for the post-ready primer
loop) and `workspaceMCPPrimeRetryInterval` (2s between empty-result
retries).
</details>
Follow-up to #25169.
---
_This pull request was generated by Coder Agents._
Mise writes a temp file like `/etc/mise/.mise.lock.XXXXXX` and renames
it onto `mise.lock` for atomic updates, which requires write access to
the parent directory. `/etc/mise` was previously root-owned (`install
--directory --mode=0755` and `COPY` without `--chown` default to root),
so any mise command that updated the lockfile failed for the coder user:
```
mise ERROR failed to update lockfiles
mise ERROR Permission denied (os error 13) at path "/etc/mise/.mise.lock.HbuLAN"
```
Chown `/etc/mise` and the baked `config.toml` / `mise.lock` to
`coder:coder`, matching how `/opt/mise` is already set up. The dogfood
image is single-user, and mise is expected to update its own lockfile
when the coder user installs new tools.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6*
The `reconciliation stats` log line runs on every reconciliation tick
(every 5 minutes by default), even when there are no presets to
reconcile. In a steady-state installation without prebuilds activity,
this is the only log line that persistently shows up at info level.
Demote it to `debug` so the steady-state log output stays quiet.
Before:
```
2026-05-14 15:01:25.085 [info] coderd.prebuilds: reconciliation stats elapsed=1.649153ms presets_total=0 presets_reconciled=0
```
After: same line is emitted at `debug` level and is suppressed at the
default info log level.
The `SectionHeadersCollapse` story (and every other
Chromatic-snapshotted story rendering the agents sidebar) was flaky
because each chat row renders `shortRelativeTime(chat.updated_at)` —
`"now"`, `"5m"`, `"7h"`, `"1d"`, etc. The values are computed against
the live wall clock from fixed fixture timestamps, so the rendered text
drifts on every run (e.g. `"7h"` → `"now"` mid-day) and Chromatic diffs
the change.
This wraps the timestamp in `<span data-chromatic="ignore">`, the
dominant codebase convention for time-varying text.
`data-chromatic="ignore"` appears 35 times across 20 files in
`site/src`, including `utils/schedule.tsx:139,153`,
`components/LastSeen/LastSeen.tsx:42`,
`modules/provisioners/Provisioner.tsx:80`,
`pages/OrganizationSettingsPage/.../ProvisionerRow.tsx:119`,
`JobRow.tsx:139`, `ProvisionerKeyRow.tsx:81`,
`pages/AIBridgePage/RequestLogsPage/RequestLogsRow/RequestLogsRow.tsx`
(multiple rows), `pages/HealthPage/WorkspaceProxyPage.tsx:144`, and
`pages/UserSettingsPage/TokensPage/TokensPageView.tsx:117`. By
comparison, only one component (`AppStatuses`) plumbs a reference date
through props, and there is no `Date.now`-stubbing precedent that would
actually stabilize `dayjs()`-based output like `shortRelativeTime`.
The wrap is scoped tightly: only the text node returned by
`shortRelativeTime` is ignored, so the sibling unread-indicator dot and
surrounding layout still participate in snapshots.
Filenames from the OS (e.g. `Screen Shot 2025-01-01 at 10.00.00 AM.png`
or `My Report (final).pdf`) flow unchanged through the chat-attach hooks
into chip labels, the persisted-attachment localStorage records, the
upload `Content-Disposition` header, and downstream LLM prompts.
Characters such as parentheses, brackets, quotes, shell or URL or path
metacharacters, whitespace, and control codes are all valid in HTTP
transport today but tend to break things further down the line (LLM tool
calls that quote the name, audit logs, any future S3/path interpolation,
shell-quoted tooling).
Sanitize at the boundary in `useFileAttachments.handleAttach` and
`useChatDraftAttachments.handleAttach` by mapping each incoming `File`
through a new `renameChatFileForUpload` helper. The helper replaces
`()[]{}<>'\"\`;,:*?|&#$\\/`, whitespace, and control characters with
`_`, collapses adjacent underscores, trims leading or trailing `_`, `.`,
or whitespace, and falls back to `"file"` if the result is empty. ASCII
alphanumerics, `.`, `-`, `_`, and all other Unicode letters and symbols
(CJK, emoji, accented Latin) are preserved so localized names remain
readable. Already-safe names return the same `File` reference; the chat
UI keys preview-URL, upload-state, and text-content Maps on the `File`
object, so identity must be stable.
The server's existing `chatfiles.NormalizeStoredFileName` (control-char
strip plus 255-byte truncate) is untouched. This is a client-only
hardening pass.
Anthropic is strict about replaying the latest assistant turn once it
contains signed or redacted reasoning. We were still mutating that turn
in a few Coder-owned places: dropping empty reasoning blocks on replay,
rewriting provider-tool history during sanitization, and in the worst
case sending a prompt we already knew Anthropic would reject.
This patch keeps the latest signed assistant immutable through Coder's
replay and sanitization paths, preserves empty signed or redacted
reasoning anywhere Coder owns the ledger, and fails before the provider
call if the prompt is still unsafe.
It also bumps the existing `coder/fantasy` `coder_2_33` fork that `main`
already uses to the commit containing coder/fantasy#35. These fixes have
also been upstreamed to charmbracelet/fantasy.
Closes CODAGT-409.
Add prev/next chevron buttons to the action row under each user message
in the agent chat transcript. Clicking jumps the scroll container to the
neighbouring user prompt's sticky sentinel (smooth scroll, no composer
mutation). Arrows disable rather than wrap when at the ends.
## Why
When a chat gets long, scrolling back to a previous prompt to see the
question that produced an answer is annoying. The transcript already has
a stable per-prompt anchor (`data-user-sentinel`) used by the
sticky-message logic, so reusing it for navigation is cheap and
consistent with the existing scroll model.
## Implementation
- `ChatMessageItem` accepts three optional props (`prevUserMessageId`,
`nextUserMessageId`, `onJumpToUserMessage`) and renders the two chevron
buttons inside the existing `message-actions` row when the message is a
user role.
- `StickyUserMessage` forwards the props to both copies of
`ChatMessageItem` (flow + sticky overlay).
- `ConversationTimeline` derives the ordered list of visible user
message IDs using the same `deriveMessageDisplayState` predicate that
controls visibility, builds a neighbour map, and supplies the jump
handler. The handler resolves the target via
`[data-user-sentinel][data-user-message-id="..."]` and smooth-scrolls
the closest `.overflow-y-auto` ancestor by the sentinel's offset
(mirroring the existing edit-flow scroll helper).
- New `data-user-message-id` attribute on the sentinel `div` to make the
lookup direct.
- New Storybook story `UserMessageJumpArrows` covers: arrow counts,
disabled-at-ends, and that clicking Next scrolls the next user sentinel
to the top of the scroller. JSDOM doesn't animate smooth scroll, so the
play function monkey-patches `scrollBy` to apply the requested top
offset synchronously.
No API, DB, or audit-table changes. Frontend only.
## Test
- `pnpm test:storybook
src/pages/AgentsPage/components/ChatConversation/ConversationTimeline.stories.tsx`
— 46 passed (incl. new story).
- `pnpm test:storybook
src/pages/AgentsPage/components/ChatConversation/` — 67 passed.
- `pnpm lint:types`, `pnpm lint:fix`, `pnpm lint:compiler`, `pnpm
format:check` — all clean.
- Local `make pre-commit` ran via the pre-commit hook on commit.
<details>
<summary>Implementation plan</summary>
Plan lives at
`/home/coder/.coder/plans/PLAN-41b442d8-05bc-4b62-b1ba-155a7cef09bc.md`
in the agent workspace. Summary:
1. Add three optional props (`prevUserMessageId`, `nextUserMessageId`,
`onJumpToUserMessage`) to `ChatMessageItem` and render
`ChevronLeft`/`ChevronRight` buttons inside the existing actions row
when the message is a user role. Disable each button when its neighbour
is undefined.
2. Forward those props through `StickyUserMessage` to both
`ChatMessageItem` instances (flow + sticky overlay).
3. In `ConversationTimeline`, build the ordered list of visible user IDs
using the same `deriveMessageDisplayState` predicate, derive a neighbour
map, and implement `handleJumpToUserMessage` that looks up
`[data-user-sentinel][data-user-message-id="${id}"]`, finds the closest
`.overflow-y-auto` ancestor, and smooth-scrolls by the sentinel's
offset.
4. Add `data-user-message-id` to the sentinel so the lookup is direct.
5. Cover the behaviour with a `UserMessageJumpArrows` Storybook play
function.
</details>
---
*This PR was authored by Coder Agents on behalf of @ibetitsmike.*
> Mux updated this PR on behalf of Mike.
## Stack Context
This stack splits experimental personal skills into smaller reviewable
PRs. Personal skills are user-owned `SKILL.md` files stored by Coder and
injected into chatd alongside workspace skills.
Stack order:
1. #25362 personal skill resolver
2. #25363 storage, permissions, API, and SDK
3. #25365 API test coverage
4. #25366 chattool and chatd integration
5. #25066 settings UI and docs
6. #25386 personal skills slash menu
## What?
Adds the shared personal skill parser and resolver package, plus
reusable skill-name validation exported from `workspacesdk`.
The parser enforces the full personal skill contract: max raw size,
kebab-case name, max name length, and non-empty body.
## Why?
The rest of the stack needs one source-aware resolver for personal and
workspace skills, including collision handling and qualified aliases.
Keeping personal skill constraints in the parser prevents callers from
accidentally parsing invalid personal skills.
## Validation
- `go test ./coderd/x/skills ./codersdk/workspacesdk`
- pre-commit hooks on this branch
This PR builds on top of https://github.com/coder/coder/pull/25070 to
add a way of running the larger "fake agent" manager via the existing
CLI, pulling in the URL/credentials already set.
With this, we can run a pod per scaletest region to act as all the
workspaces in that region.
This is in a new subcommand `scaletest agentfake` currently.
---------
Signed-off-by: Callum Styan <callumstyan@gmail.com>
The dogfood Dockerfiles consume the repo-root `mise.toml` and
`mise.lock` at build time (see `.dockerignore` allowlist), but the
template's `pull_triggers` list ignored them, so mise-only changes (tool
bumps, new tools) didn't roll out to existing workspaces.
Mirror the `nix.hash` pattern: a Makefile rule writes the sha256 of both
files into `dogfood/coder/mise.hash`, and `main.tf` hashes that
in-module file via `filesha1`. Run `make dogfood/coder/mise.hash` after
editing `mise.toml`/`mise.lock`.
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
*Disclaimer: implemented by a Coder Agent using Claude Opus 4.6*
The `noEmptyInterface` rule is enabled by default as a recommended Biome
rule. Its safe fix converts empty interfaces to type aliases (e.g.
`export interface X {}` becomes `export type X = {}`), which then
conflicts with `noBannedTypes` on the empty `{}` type. Since empty
interfaces are legitimately produced by Go-to-TypeScript code
generation, this disables the rule entirely.
<details><summary>Context</summary>
Discussion in Slack concluded that disabling the rule is preferable to
adding per-type exceptions in `scripts/apitypings/main.go`, since the
safe fix itself creates a worse lint violation.
</details>
Follow-up to #25387. The top-level `[env] CGO_ENABLED = "1"` was
re-exported through every mise shim at runtime, forcing cgo on for `go
build` calls and breaking cross-compilation of coderd (slim builds,
`./scripts/develop.sh`) on the dogfood image.
`scripts/build_go.sh` sets `CGO_ENABLED=0` explicitly for
non-boringcrypto builds, but the mise shim overwrites it back to `1`
before `go` runs. Scope the variable to sqlc's `install_env` so it only
applies during the one `go install` that needs it.
Ref: https://mise.en.dev/configuration.html#tools-dev-tools
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Summary
- Wire the Appearance settings page to the new theme mode dropdown and
sync or single theme selectors.
- Update Appearance page tests and stories for theme mode behavior.
- Update the user settings e2e test to exercise single theme selection.
## Dependencies
- Depends on #25076, #25180, #25181, and #25182.
- This PR targets helper branch `pr25077/05-theme-mode-dropdown-base`,
which contains dependency commits only, so this PR diff stays focused on
final dropdown wiring. Rebase and retarget after the dependency PRs
merge.
## Validation
- `pnpm -C site exec vitest run --project=unit
src/pages/UserSettingsPage/AppearancePage/AppearancePage.test.tsx
src/theme/themeMode.test.ts src/api/queries/users.test.ts`
- `pnpm -C site lint:types`
- `pnpm -C site storybook:ci`
- `pnpm -C site build`
- `pnpm -C site playwright:test -- e2e/tests/users/userSettings.spec.ts`
- Pre-commit hook passed on the branch commit.
Three changes to make mise-managed tooling reach every dogfood workspace
cleanly, with the upstream `devcontainers-cli` module fix as the
original trigger.
## Why the module breaks
The upstream [`devcontainers-cli` coder
module](https://github.com/coder/registry/blob/main/registry/coder/modules/devcontainers-cli/run.sh)
does `npm install -g @devcontainers/cli` and then verifies the binary is
on `PATH`. With mise-managed Node (introduced in #25282), `npm install
-g` lands the binary at `$MISE_DATA_DIR/installs/node/<ver>/bin/`, which
is *not* on `PATH` and which `mise reshim` does not surface as a shim.
The post-install check fails:
```
Installing @devcontainers/cli using npm...
changed 1 package in 661ms
Reshimming mise 26...
Installation completed but 'devcontainer' command not found in PATH
```
Even though nothing the user does is actually broken.
## What this PR does
1. **`mise.toml`** — pre-install `@devcontainers/cli` via mise's `npm:`
backend (`npm:@devcontainers/cli = "0.87.0"`). The mise shim lands at
`$MISE_DATA_DIR/shims/devcontainer`, on `PATH`. The upstream module's
`run.sh` short-circuits on its `command -v devcontainer` check and exits
0 without ever running the broken npm-install path. Strictly redundant
after fix the second point makes `npm i -g` work natively, but kept for
build-time pre-install and pinned-version reasons matching the other
mise-pinned CLIs.
2. **`dogfood/coder/ubuntu-*.04/Dockerfile`** — set
`NPM_CONFIG_PREFIX=/home/coder/.npm-global` and prepend
`/home/coder/.npm-global/bin` to `PATH`. With this, generic `npm install
-g <pkg>` (prettier, biome, anything frontend folks reach for) lands in
a stable home-volume dir that is already on `PATH`, survives node
version bumps, and needs no `mise reshim`. The mise `npm:` backend keeps
using its own `--prefix` internally so the `npm:@devcontainers/cli` pin
still installs under `$MISE_DATA_DIR` as before.
3. **`dogfood/coder/ubuntu-*.04/Dockerfile`** — install image tools into
`/opt/mise/data` at build time (owned by `coder`) and expose them at
runtime via `MISE_SHARED_INSTALL_DIRS=/opt/mise/data/installs`, keeping
`MISE_DATA_DIR=/home/coder/.local/share/mise` for the user's own
installs. This decouples baked tool versions from the home volume's
copy-on-first-mount: fresh and existing workspaces both immediately see
the image's tool set without a `mise install` step, and the user's own
`mise install <tool>` / `mise use --global` still lands on the home
volume. The `/opt/mise/data/shims` dir trails the user shim dir on
`PATH` so a user-installed version wins when both exist.
Pinned to `0.87.0` (current latest) so Renovate/Dependabot can bump
deliberately, matching the policy applied to the other floating tools
during the mise migration (`lazygit`, `doctl`, `jj`, `typos`,
`watchexec`).
---------
Signed-off-by: Thomas Kosiewski <tk@coder.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## Problem
`Docs CI` fails on PRs that only touch binary assets under `docs/`.
Example: [#25314](https://github.com/coder/coder/pull/25314), which
swaps a single PNG and produces thousands of `MD010/no-hard-tabs`,
`MD049/emphasis-style`, and `MD018/no-missing-space-atx` errors at
columns like 16,285 of the image.
## Root cause
The single `tj-actions/changed-files` step was doing two jobs at once:
detecting which Markdown files changed (for `lint` and `fmt`), and
gating whether the workflow had anything to do at all. Its `files`
filter matched `docs/**` in addition to `**.md`, so any non-Markdown
file under `docs/` (PNG, GIF, JPG, MP4, SVG) ended up in
`all_changed_files` and was passed straight to `markdownlint-cli2`,
which opened the file and parsed the binary bytes as Markdown.
`markdownlint-cli2`'s own `ignores` setting is a discovery-time filter
and does not gate files passed explicitly on the command line, so the
filtering has to happen in the caller.
## Fix
Adopt a per-tool convention: each downstream tool gets its own
`changed-files` step scoped to the files that tool can process. For now
that is a single `changed-md` step matching `**.md`, consumed by `lint`
and `fmt`. A future tool (e.g. an image linter, video size check, or
link checker) can be added purely additively, by appending another
`changed-*` step and a step that consumes its output, without changing
the existing filters.
The workflow-level `on.push.paths` / `on.pull_request.paths` triggers
stay broad (`docs/**`, `**.md`) so the workflow still runs on
screenshot-only PRs; the per-tool filters decide which individual steps
execute. On a screenshot-only PR the existing `if:
steps.changed-md.outputs.any_changed == 'true'` guard skips `lint` and
`fmt` cleanly.
## Verification
- `actionlint .github/workflows/docs-ci.yaml` passes.
- Reproduced the original failure locally: `pnpm exec markdownlint-cli2
docs/images/install/install_from_deployment.png` produces the same flood
of violations seen in the failing CI run on #25314.
- First revision of this PR (workflow with `**.md`-only filter, single
`changed-files` step) was green on `Docs CI`; the current revision is
structurally equivalent for the existing tools and just renames the step
id and adds the per-tool comment.
<details>
<summary>Decision log</summary>
- Considered adding `ignores` to `.markdownlint-cli2.jsonc` to skip
non-Markdown files. Rejected: `markdownlint-cli2` treats `ignores` as a
discovery-time glob filter and still lints files passed explicitly on
the command line, so it would not have fixed the failure.
- Considered narrowing the existing single `changed-files` step's
`files` filter to `**.md` only. Rejected as the final shape: it solves
the immediate bug but conflates "which Markdown files changed" with
"should the workflow run at all", so adding a second tool with a
different file set later (e.g. an image linter) would require contorting
or duplicating that step.
- Chose the per-tool-filter shape so adding a future tool is additive:
one new `changed-*` step plus one new step that consumes its output,
with no edits to existing steps.
</details>
## Disclosure
Opened on behalf of @nickvigilante by Coder Agents.
Fixescoder/internal#1535
## Problem
`TestPatchChatMessage/ChangesModel` is flaky because it races with the
chat daemon's background processing.
`CreateChat` sets the chat to `pending` and the daemon picks it up
asynchronously. The test immediately calls `EditChatMessage` (which
changes the model to an override) while the first processing round is
still running. The `InsertChatMessages` SQL CTE unconditionally updates
`chats.last_model_config_id` to the model of the last inserted message.
When the daemon's in-flight message insertions commit after the edit
transaction, they overwrite `last_model_config_id` back to the default
model.
Similarly, after the edit sets the chat back to `pending`, the daemon
re-processes it. The test's `GetChat` call could race with this second
round.
## Fix
Poll for the chat to reach `waiting` (or `error`) status:
1. **Before editing**: wait for the initial processing round to complete
2. **After editing**: wait for the second processing round (triggered by
the edit) to complete
Then assert `last_model_config_id`, which is now stable.
> Generated with [Coder Agents](https://coder.com/agents) by @kylecarbs
Three workflows besides `deploy-docs.yaml`
([DOCS-124](https://linear.app/codercom/issue/DOCS-124),
[#25285](https://github.com/coder/coder/pull/25285)) self-reference in
their `paths:` triggers: `docker-base.yaml`, `docs-ci.yaml`,
`dogfood.yaml`. This was flagged during review of #25285
([DEREM-1](https://github.com/coder/coder/pull/25285#discussion_r3234975475))
as a bug class worth treating uniformly. This PR is the audit.
Each self-reference is either justified inline or removed:
* **`docker-base.yaml`** keeps the self-reference. It's PR-only and
gated by `push: ${{ github.event_name != 'pull_request' }}` on the
`depot/build-push-action`, so PRs build the base image without
publishing.
* **`docs-ci.yaml`** drops the self-reference. The `lint` and `fmt`
steps gate on `tj-actions/changed-files` matching `docs/**` or `**.md`,
so a workflow-only run no-ops. `actionlint` and `make lint/actions`
catch YAML problems before merge regardless.
* **`dogfood.yaml`** keeps the self-reference. PR runs build images
without pushing and run `terraform init` + `validate` only; pushes to
main retag rolling tags on `codercom/oss-dogfood`,
`oss-dogfood-vscode-coder`, and `oss-dogfood-nix`, plus `terraform
apply` against dev.coder.com which produces new `coderd_template`
versions with unchanged content. Idempotent and bounded.
Refs DOCS-121, DOCS-129.
<details>
<summary>Decision table</summary>
| Workflow | Self-ref location | Effect on workflow-only edit | Decision
|
|---|---|---|---|
| `deploy-docs.yaml` | push + workflow_dispatch | Destructive (DOCS-121)
| Removed in [#25285](https://github.com/coder/coder/pull/25285) |
| `docker-base.yaml` | PR-only | Build base image, never push | Keep
with inline comment |
| `docs-ci.yaml` | push + PR | Empty run; lint/fmt skipped by `if:` |
Remove (wasted runner minutes) |
| `dogfood.yaml` | push + PR | PR: build without push, terraform
validate. Main: retag rolling tags, terraform apply, new cosmetic
template versions | Keep with inline comment |
</details>
---
_Coder Agents on behalf of @nickvigilante._
Edits to `.github/workflows/deploy-docs.yaml` previously self-triggered
the workflow on push to `main` and `release/*` because the file was
listed in its own `paths:`. On 2026-05-12, this caused merge of #25049
to fire a production reindex with no `docs/**` changes, which entered
the empty-`paths_json` whole-branch path in the Algolia handler and
wiped the `docs` index (see DOCS-121).
This change removes `.github/workflows/deploy-docs.yaml` from `paths:`
so the workflow only runs against real docs content. Reindexes from a
workflow edit alone now require `workflow_dispatch`, which already
accepts a `ref` input and an `action` choice of `index` or `delete`. The
other safety net (a workflow-level `paths_json=[]` guard in
`algolia-and-isr`) is tracked separately in DOCS-122.
Refs DOCS-121, DOCS-122, DOCS-124.
---
_Coder Agents on behalf of @nickvigilante._