6487 Commits
Author SHA1 Message Date
Steven Masley 03cba25a63 feat: list a workspace build's secrets in coder secret list (#30388)
`coder secret list` now shows the current workspace build's secrets next to your user secrets, and which user secrets they replace. Part of [RELAY-7](https://linear.app/codercom/issue/RELAY-7/agent-relay-deliver-provider-credentials-via-coder-secrets-instead-of).

**Stack** (design decisions in #30248)
1. #30248 Database schema, queries, and RBAC
2. #30249 Encryption at rest
3. #30250 Set secrets on workspace builds
4. #30251 Deliver secrets to the agent
5. #30252 Docs
6. **#30388 List a build's secrets (this PR)**
7. #30459 Audit a build's secrets

- New `GET /workspacebuilds/{workspacebuild}/secrets` lists a build's secrets (metadata only, never values) and which user secret each one replaces.
- Inside a workspace, `coder secret list` includes the current build's secrets. Outside a workspace, use `--workspace` or `--build-id`.
- The agent now exposes `CODER_WORKSPACE_BUILD_ID`, so the CLI knows which build it is running in.
- The agent manifest and the new endpoint use one shared resolver, so they always agree on which secret wins.
- The user secret API is unchanged.

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

- The manifest carries `workspace_build_id`; the agent sets it as `CODER_WORKSPACE_BUILD_ID`.
- `coderd/workspacesecrets.Resolve(user, build, policy)` decides which secret is delivered on each env var and file. The manifest no longer relies on entry order.
- `codersdk.WorkspaceSecret` embeds `UserSecret` and adds `workspace_build_id`, `ephemeral`, `cleared_at`, `env_replaces`, and `file_replaces`. Secrets whose values a later build cleared are still listed, with `cleared_at` set.
- CLI columns: `source` and `replaced by` are shown by default only when a build is included; `ephemeral` is opt-in. The CLI makes two requests: user secrets, then build secrets.
- Agents that predate the build ID fall back to `CODER_WORKSPACE_ID` and its latest build.

</details>

<details>
<summary>Decision log</summary>

(existing decision log, unchanged)

</details>

---

*Created by Coder Agents on behalf of @Emyrk.*
2026-10-15 17:40:00 -05:00
Jake Howell 3b283eb047 fix(site): preserve personal instructions drafts (#30626)
> 🤖 This PR was modified by Coder Agents on behalf of Jake Howell.

Prevent personal instructions drafts from being overwritten by delayed
query results. Block editing until instructions load, show initial-load
errors distinctly from empty prompts, and preserve dirty edits during
later query updates.

Reset successful saves from the server response and cache it before
refetching so a failed refresh cannot restore stale instructions.
Includes behavioral tests and loading, error, and empty Storybook
states.

Reproduction:

1. The user already has a nonempty saved personal-instructions prompt.
2. Open settings with the initial GET delayed.
3. Type a new draft into the enabled textarea.
4. The GET returns the existing saved prompt.
5. Formik replaces the draft with that prompt.
6. Further typing and Save submit the saved prompt plus subsequent
edits, losing the original draft.

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

- Keep Formik reinitialization for initial hydration and pristine
updates, guarded by the existing personal-model form draft pattern.
- Use the confirmed mutation response as the clean form baseline and
cache value.
- Leave the existing Saved-indicator behavior unchanged; no additional
retry UI.

</details>
2026-10-16 09:13:28 +11:00
Andrew Aquino 5261723186 fix(site/src): replace role="application" on recharts charts (#30375)
> [!NOTE]
> This PR was generated by Coder Agents on behalf of @aqandrew.

Fixes DEQ-408 (axe `custom-role-application` on the user engagement
chart). Also fixes DEQ-342

recharts' `accessibilityLayer` sets `role="application"` on the chart
`<svg>` by default. This PR overrides it on all three charts that use it
(user engagement, template active users, license seat consumption) with
`role="group"` and `aria-roledescription="chart"`. It also gives each
chart an accessible name and description through recharts'
`title`/`desc` props.

`accessibilityLayer` stays, so keyboard users can still move the tooltip
with the arrow keys. To make that work for screen readers,
`ChartContainer` now renders a polite `role="status"` live region that
is always mounted. `ChartTooltipContent` builds the announcement during
render from the same label and item values the tooltip shows, and an
effect keyed on that string pushes it into the live region. Each point
is announced, for example "175 users January 02".

Not verified: NVDA behaviour (whether Tab focus switches to focus mode
so the arrows reach the chart), and an axe re-run on
`/deployment/overview`.

Frontend-review notes:
- **FE1:** the assertions live in story `play` functions. recharts'
keyboard navigation depends on real layout (`getBoundingClientRect`), so
jsdom unit tests can't exercise it.
- **FE10:** `<desc>` is checked with a selector. `dom-accessibility-api`
doesn't compute descriptions from SVG `<desc>`, although Chromium does
per SVG-AAM.
- **FE8:** the announcement string is derived during render. The one
remaining effect only pushes it into `ChartContainer`'s state, because a
component can't update another component's state while rendering.

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

## Findings (recharts 2.15.4, verified in `node_modules`)

- `generateCategoricalChart.js:1937-1953`: with `accessibilityLayer`,
the `<svg>` gets `tabIndex = props.tabIndex ?? 0`, `role = props.role ??
"application"`, and `onKeyDown`/`onFocus` handlers. The handlers are
attached whatever the role is, so `role="group"` keeps
ArrowLeft/ArrowRight working.
- Keyboard navigation (`AccessibilityManager.spoofMouse`) goes through
the same `triggeredAfterMouseMove` path as the mouse, so the tooltip
shows the same content for both.
- `container/Surface.js:41`: the `title` and `desc` props render as
`<title>` and `<desc>` children of the `<svg>`. These give the SVG its
accessible name and description.
- `component/Tooltip.js`: `content` is always rendered and receives an
`active` prop. The tooltip wrapper (`TooltipBoundingBox`, `tabIndex=-1`)
is a sibling of the `<svg>` and is `visibility: hidden` when inactive.
That makes it unreliable as a live region, so announcements need their
own element that is always mounted.
- Three charts use `accessibilityLayer`: `UserEngagementChart`
(flagged), `ActiveUserChart`, `LicenseSeatConsumptionChart`. All three
are fixed.

## Changes

1. **Shared live region in `Chart.tsx`:** `ChartContainer` holds the
announcement state and renders an always-mounted `sr-only` live region
next to `ResponsiveContainer`. `ChartTooltipContent` sets the
announcement while active and clears it when inactive.
2. **Per-chart props:** keep `accessibilityLayer`; add `role="group"`,
`aria-roledescription="chart"`, `title` (the section name), and `desc`
(date range plus "Use the left and right arrow keys to read each day.").

## Tests (red, then green)

`play` functions on the existing stories, which run in Chromium through
`@storybook/addon-vitest`:
- **User engagement:** the chart is a named group with roledescription
"chart" and the expected `<desc>`. There is no `application` role.
Focusing the chart and pressing ArrowRight updates the live region.
- **The other two charts:** role and name only.
- **`components/Chart` default story:** without formatters, the
announcement is built from the label, the item name and the value
("January Users 186").

With the source changes reverted, all three tests fail.

## Decision log

- **Role:** `group` rather than `img`, because `img` hides the content
and conflicts with the focusable, arrow-key-driven chart. `figure` has
no interaction meaning, and `graphics-document` has poor screen-reader
support.
- **Keep the arrow-key tooltip** for keyboard users without a screen
reader, and announce changes through a live region.
- **Add `aria-roledescription="chart"`**, so screen readers say "chart"
rather than "grouping".
- **Memoize the `ChartContext` value** in `ChartContainer`, so
announcement updates don't re-render every `useChart` consumer.
`components/Chart` isn't covered by the React Compiler.
- **Deviations from the plan:**
- The live region gained `role="status"`, so it can be queried by role.
- The plan keyed the announcement effect on `active` and `payload`. The
first version instead ran on every render and read the text from the
DOM. Following review, the string is now derived from the label and item
values (strings and numbers only), and the effect depends on that
string.
- The test focuses the chart directly instead of pressing Tab, because
the first Tab stop is the "How we calculate" button.
- The description is checked on `<desc>` rather than with
`toHaveAccessibleDescription` (see FE10 above).
- The planned "live region empties on leave" assertion was dropped,
because recharts doesn't clear the tooltip on blur.

## Known tradeoffs

- **Mouse hover:** moving the mouse over the chart also triggers
announcements for screen-reader users, once per point.
- **NVDA:** NVDA users need to be in focus mode for the arrow keys to
reach the chart. The `desc` hint covers this.
- **No data table:** this PR doesn't add a table alternative to the
chart, so the WCAG 1.1.1 risk for the data itself remains.

</details>
2026-10-15 14:02:57 -07:00
Steven Masley f25b6294b6 feat: set workspace secrets on workspace builds (#30250)
Lets clients set secrets when creating a workspace or a build. Secrets are saved with the build and are never sent to the provisioner. Part of [RELAY-7](https://linear.app/codercom/issue/RELAY-7/agent-relay-deliver-provider-credentials-via-coder-secrets-instead-of).

**Stack** (design decisions in #30248)
1. #30248 Database schema, queries, and RBAC
2. #30249 Encryption at rest
3. **#30250 Set secrets on workspace builds (this PR)**
4. #30251 Deliver secrets to the agent
5. #30252 Docs
6. #30388 List a build's secrets
7. #30459 Audit a build's secrets

- `secrets` is a new field on the create workspace and create build requests.
- Each secret is delivered as an env var, a file, or both. `ephemeral` secrets last for one build only.
- Secrets carry forward to every later build until a request replaces or removes them.
- A secret set on another secret's env var or file path replaces that secret entirely.
- A null `value` removes a secret, which needs `update` permission on the owner's workspace secrets.
- Deleting a workspace clears its secret values.

Secrets are stored but not delivered to the agent until #30251.

<details>
<summary>Example request</summary>

```json
POST /api/v2/workspaces/{workspace}/builds
{
  "transition": "start",
  "secrets": [
    { "name": "openai-key", "value": "sk-...", "env_name": "OPENAI_API_KEY" },
    { "name": "kubeconfig", "value": "apiVersion: v1 ...", "file_path": "~/.kube/config" },
    { "name": "session-jwt", "value": "eyJ...", "env_name": "SESSION_JWT", "ephemeral": true },
    { "name": "old-token", "value": null }
  ]
}
```

</details>

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

- Validation reuses the user secret field rules and the file path delivery policy. It rejects:
  - duplicate names, env names, or file paths within one request
  - a removal that also sets a target or `ephemeral`
- In the build transaction, the builder:
  1. copies the previous build's live, non-ephemeral secrets that the request does not name or take over
  2. inserts the requested secrets
  3. "Earlier builds' rows are cleared when the new build's job completes
- Copying and clearing run as `AsWorkspaceSecretManager`, so autostart and other system-initiated builds work without secret permissions. Requested secrets are authorized as the build's actor.
- Each row records `source` (`request` or `carry_forward`).
- Per-build limit violations return a 400.
- A completed delete in `provisionerdserver`, or an orphan delete in the builder, clears the workspace's values and keeps the rows.

</details>

---

*Created by Coder Agents on behalf of @Emyrk.*
2026-10-15 14:20:44 -05:00
Steven Masley 7aef110ca2 feat: add workspace secrets table, queries, and RBAC (#30248)
Adds the database table, queries, and permissions for workspace secrets: secrets set on a workspace build and delivered to the agent, never to Terraform. 

Each secret belongs to one workspace build. A new build copies the previous build's secrets, then the old copies lose their values but stay as history.
2026-10-15 13:32:40 -05:00
Nick Vigilante c6a47c4428 docs: replace references to the retired Enterprise license tier (#30853)
## Summary

Coder has only Community and Premium license tiers, but several docs
pages, CLI help strings, and SDK field comments still called features
"Enterprise" or "enterprise-only".
This PR changes them to say Premium and regenerates the reference docs
built from those strings.

Resolves
[DOCS-1298](https://linear.app/codercom/issue/DOCS-1298/remove-references-to-the-retired-enterprise-license-tier).

## Changes

- `docs/admin/users/quotas.md`: Replace "scoped to Groups in Enterprise
and organizations in Premium" with a correct sentence. Quotas are
Premium, allowances are set on groups, and each user's budget is
calculated separately in each organization (per
`GetQuotaAllowanceForUser`).
- `docs/user-guides/workspace-lifecycle.md`: Replace "Enterprise" with
Premium, and correct the quotas sentence to match the quotas page.
- `docs/ai-coder/agents/platform-controls/mcp-servers.md`: Replace
"enterprise entitlement" with Premium license.
- `cli/templatecreate.go`, `cli/templateedit.go`: Update
`--require-active-version`, `--allow-user-autostart`, and
`--allow-user-autostop` help text. Point the `--require-active-version`
help at the renamed `#template-update-policies` section, because the old
`#require-automatic-updates-enterprise` anchor is broken. Make the
errors for these flags tier-neutral.
- `enterprise/cli/features.go`: `coder features` now says "List license
features and entitlements", and its AGPL error no longer names a tier.
- `codersdk/templates.go`, `codersdk/organizations.go`: Update field
comments that feed the API reference and TypeScript types.
- `docs/reference/glossary.md`: The Premium entry now says "paid license
tier" instead of "paid enterprise tier".
- `docs/.style/style-guide/voice-and-tone.md`: The example sentence now
says "Premium licensing".
- Regenerated: CLI golden files, `docs/reference/cli/`,
`docs/manifest.json`, `docs/manifest/generated/cli.json`,
`docs/reference/api/`, `coderd/apidoc/`, and
`site/src/api/typesGenerated.ts`.

## Testing

- `go test ./cli -run 'TestCommandHelp|TestCliTemplateCreate'` and `go
test ./enterprise/cli -run
'TestEnterpriseCommandHelp|TestTemplateCreate|TestTemplateEdit|TestFeaturesList'`
pass.
- The pre-commit hook passed, including `make gen`, `fmt`, `lint/go`,
`lint/ts`, `lint/markdown`, and a slim build.

<details>
<summary>Decisions and out-of-scope items</summary>

### Decisions

- **Premium wording and legacy Enterprise licenses:** Coder still
accepts existing Enterprise licenses, and `FeatureSetEnterprise`
includes the features labeled Premium here. Docs and help text say
"Premium" because that is the tier sold today, and about 25 other docs
pages already use "Premium feature". Error strings are tier-neutral, so
they're accurate for every license holder.
- **Glossary and style guide:** The issue marked these as borderline.
Both were one-word changes, so I made them.

### Not changed

- `docs/reference/api/enterprise.md` keeps the "Enterprise" sidebar
title. It comes from the swagger tag on the `enterprise/` Go package, so
renaming it is a larger change.
- Other "Enterprise" references the review flagged as out of scope: the
`coder server --help` option group, quiet hours SDK comments, internal
`coderd` comments, and the dashboard's "Enterprise" badge on the
require-active-version option.
- No docs page says that legacy Enterprise licenses are still valid.
- Uses that don't refer to the license tier: GitHub Enterprise, Red Hat
Enterprise Linux, "enterprise environments", and `enterprise/` package
paths.

</details>

---

_This PR was generated by Coder Agents on behalf of @nickvigilante._
2026-10-15 17:46:05 +00:00
cdrciandgithub-actions[bot] 7e69cd0af7 chore: refresh AI model price book (#30828)
## Price book changes

0 models added, 0 models removed, 24 models changed.

<details>
<summary>Changed</summary>

- openrouter/deepseek/deepseek-v4-pro
- openrouter/deepseek/deepseek-v4-pro-0813
- openrouter/deepseek/deepseek-v4.1-flash
- openrouter/google/gemma-4-31b-it
- openrouter/minimax/minimax-m2.5
- openrouter/mistralai/mistral-small-3.2-24b-instruct
- openrouter/moonshotai/kimi-k3
- openrouter/openai/gpt-5.6-sol
- openrouter/openai/gpt-5.6-sol-pro
- openrouter/openai/gpt-oss-120b
- openrouter/qwen/qwen3.6-35b-a3b
- openrouter/qwen/qwen3.8-27b
- openrouter/tencent/hy3
- openrouter/tencent/hy4-preview
- openrouter/z-ai/glm-5
- openrouter/z-ai/glm-5.1
- openrouter/z-ai/glm-5.2
- openrouter/z-ai/glm-5.3
- openrouter/~deepseek/deepseek-flash-latest
- openrouter/~deepseek/deepseek-pro-latest
- openrouter/~google/gemini-pro-latest
- openrouter/~moonshotai/kimi-latest
- openrouter/~z-ai/glm-flash-latest
- openrouter/~z-ai/glm-latest
</details>

## Review notes

Regenerated by `make gen/aibridge-prices` from the live
[models.dev](https://models.dev) catalog. Both artifacts come from one
snapshot, so they ship together:

- `coderd/aibridge/prices/data/prices.json`
- `site/src/modules/aiModels/knownModels/knownModelsGenerated.json`

These are customer-visible cost numbers taken from upstream data, so
this PR is never merged automatically. The summary above lists what
moved; check the diff for exact figures before approving.

Opened automatically by the [aigateway-prices-refresh
workflow](https://github.com/coder/coder/actions/runs/38710433812).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-15 12:01:26 +01:00
Andrew Aquino 1d57682dab fix: give buttons accessible names (#30796)
Fixes the axe `button-name` (WCAG 4.1.2) findings from the Deque audit:

- fixes DEQ-431
- fixes DEQ-456
- fixes DEQ-101 (duplicate of DEQ-456)
- fixes DEQ-138

affected buttons:

- **Workspace sharing role selects** (DEQ-431, DEQ-456, DEQ-101): Radix
Select triggers render `role="combobox"`, and ARIA doesn't let a
combobox take its name from its content. That's why the visible
"Use"/"Admin" text didn't count. Each member row's select is now named
`Role for <member>`. The add-member form's select is named `Role`.
- **Agent logs "Copy selected logs" button** (DEQ-138): the icon-only
button had a name only in its tooltip. It now has `sr-only` text,
matching the download button next to it.

Adds Vitest coverage in `WorkspaceSharingForm.test.tsx`. The test finds
each select by its accessible name and checks that changing the role
calls the update callback. It fails without the fix.

<details>
<summary>Implementation notes and review decisions</summary>

**Mapping axe selectors to components**

- DEQ-431: `form > div > button:nth-child(2)` is `RoleSelectField` in
`AddWorkspaceUserOrGroup`.
- DEQ-456 / DEQ-101: `table > tbody > tr > td:nth-child(2) > button` is
`RoleSelect` in `WorkspaceSharingForm`.
- DEQ-138: `section:nth-child(3)` (agent card logs section) `> … >
div:nth-child(2) > button:nth-child(1)` is the copy button in the
`AgentRow` logs toolbar. `data-state="closed"` with no `aria-expanded`
means it's a Tooltip trigger.

**Justified findings from the frontend review (`frontend-deep-review`
and `frontend-review`)**

- FE1/TS5, no Vitest for the copy button: rendering `AgentRow` with
streamed logs needs the proxy context and websocket log fixtures. The
change only adds an `sr-only` label. I removed a story `play` assertion
that I'd first added, because FE1 bans assertions in `play` functions.
- FE6, label-in-name (WCAG 2.5.3): the combobox's visible text is its
*value* ("Use"/"Admin"), not its label. The visible label is the "Role"
column header, and the accessible names include it. Screen readers still
announce the value from the trigger's content.
- Not changed: the copy button's tooltip switches to "Copied!" but
screen readers hear no announcement. That was already the case before
this PR, and it's a different WCAG criterion (4.1.3), so it's out of
scope here.

**Checks run:** `pnpm check`, `pnpm lint:types`, `pnpm lint`, the unit
tests, the `AgentRow` / `WorkspaceSharingPage` / `WorkspaceActions`
story tests, and `make pre-commit`.

</details>

---

_Generated by Coder Agents on behalf of @aqandrew._
2026-10-14 15:58:33 -07:00
Michael Suchacz e7c2fd5d2e feat: add Gemini 4 Argon to Agents and AI Gateway (#30780)
Add Gemini 4 Argon to the Agents known models catalog, so it appears as
a suggestion when an admin adds a chat model, and regenerate the AI
Gateway price book from models.dev.

| Provider | Model ID | Placed before |
|---|---|---|
| google | `gemini-4-argon` | `gemini-3.1-pro-preview` |
| openrouter, vercel | `google/gemini-4-argon` |
`google/gemini-3.8-flash` |

**Pricing.** The price book carries Google's launch rates on google,
openrouter, vercel, and copilot: $2 per 1M input tokens, $10 per 1M
output tokens, and $0.10 per 1M cached input tokens. Google doubles them
on 2026-12-01 ($4, $20, $0.20); the weekly refresh picks that up once
models.dev changes. The google entry keeps models.dev's 32,768 max
output tokens, the cap Google enforces on non-streaming
`generateContent` calls (larger values return HTTP 400 there).

**Thinking levels.** `googleSupportedThinkingLevels` did not recognize
the `argon` name token and failed closed, so chatd sent Argon no
`thinking_level` and no `include_thoughts`: the native Google path
dropped the reasoning effort, and neither path showed thought summaries.
Argon accepts LOW, MEDIUM, and HIGH like Gemini 3.1 Pro and rejects
MINIMAL, so the map now treats it like the Pro family.

**Upstream drift.** `prices.json` covers every model of its 8 providers,
so the regeneration also brings the changes since the last weekly
refresh, all on OpenRouter and Vercel: 14 other new entries, 26 entries
models.dev delisted, and 51 price changes. The catalog picks up the same
changes for 8 curated OpenRouter entries (DeepSeek, Kimi, Qwen, and
GLM).

## Testing

- Live requests to `gemini-4-argon` through the AI Gateway's Google
provider (Google's OpenAI-compatible endpoint): `reasoning_effort` and
`thinking_level` values `low`, `medium`, and `high` return 200 and
`minimal` returns 400; `include_thoughts` returns thought text in
streaming and non-streaming responses.
- Red-green: removing the `argon` token fails
`TestGoogleSupportedThinkingLevels/gemini-4-argon` (expected LOW,
MEDIUM, HIGH; got nil).
- Regenerating from the saved models.dev snapshot reproduces both
generated files byte for byte.
- `go test ./scripts/aibridgepricesgen/... ./coderd/aibridge/prices/...
./coderd/x/chatd/chatprovider/...` and `pnpm exec vitest run
--project=unit src/modules/aiModels/` pass. The pre-commit hook passed;
local pre-push checks were skipped, so CI is the first full run.

## Review record

- Frontend review: the only `site/` change is the generated
`knownModelsGenerated.json`, so the FE1 to FE10 checklist does not
apply; no hand-written frontend code changed.

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-14 22:39:00 +02:00
Andrew Aquino 4f7bddb640 fix(site/src/pages/TemplateBuilder): use a radio group for module category filters (#30523)
Fixes the axe `aria-valid-attr-value` violation reported in DEQ-288 on
the template builder's "Select modules" step.

The category filters were Radix `Tabs` with no `TabsContent`. Radix
`TabsTrigger` always sets `aria-controls` to `<baseId>-content-<value>`,
so the active "All" trigger pointed at an id that did not exist. (axe
skips `aria-controls` on tabs with `aria-selected="false"`, which is why
only the active one was flagged.)

The categories filter the same grid as the search field, so they are not
tabs and there is no panel per category. They now render as a Radix
`RadioGroup` (`role="radiogroup"` with `role="radio"` / `aria-checked`
items) labelled "Filter modules by category". Arrow keys move focus and
check the item, following the APG radio pattern. Keyboard focus shows
the same ring as `TabLink`. The styling matches the previous tabs;
computed colors, borders, padding, and positions are identical. The
shared `#/components/RadioGroup` wrapper renders a dot indicator, so
this uses the Radix primitive directly.

The `useKebabMenu` call is removed with the tabs. It was never wired up
here (no measure props and no overflow menu rendered), and the row
already wraps.

Verification:
- Ran axe-core 4.11 against the `Default` and `FilteredByCategory`
stories. On `main` it reports `aria-valid-attr-value` on
`#radix-…-trigger-All`. On this branch that violation is gone. The
remaining `nested-interactive` and `target-size` findings come from
`ModuleCard` and are out of scope.
- Keyboard check in Playwright: Tab lands on the checked radio,
ArrowRight/ArrowLeft check the adjacent category and filter the grid,
and Tab leaves the group in one stop. The focus ring shows on keyboard
focus only, not on mouse click.
- `pnpm test:storybook` for `ModuleSelectStep.stories.tsx` (5 passed),
`pnpm lint:types`, `pnpm check`, `pnpm lint`.

> 🤖 This PR was generated by Coder Agents on behalf of @aqandrew.
2026-10-14 11:47:12 -07:00
Danielle Maywood b8c9195b3f fix(site/src/pages/AgentsPage): make git panel refresh match a page reload (#30678)
The git panel's refresh button invalidated the PR diff contents but not
the chat, whose `diff_statuses` back the PR tabs, so a page reload could
show PR state the button missed. Refresh now also invalidates the chat
entity.

The button was also disabled until the panel had git context, which left
no way to pick up a repo created after the chat touched its path. It is
now always enabled; the diff style toggles stay disabled without git
context.

### Before (Scenario A)


https://github.com/user-attachments/assets/82dc7c48-148f-4661-af73-8400a275b05d

### After (Scenario A)


https://github.com/user-attachments/assets/0f24e390-39d3-4b43-9f4c-63d499837e33

### Before (Scenario B)


https://github.com/user-attachments/assets/a779031a-8bb9-466b-9cf3-a56ad02a98d5

### After (Scenario B)


https://github.com/user-attachments/assets/05c081e7-7ad1-4608-8ef7-1bac4940034c

<details>
<summary>Decision log</summary>

The git panel's refresh button should show everything a page reload
would. Investigation found five ways it didn't, each fixed in its own PR
on the current `changes` protocol:

1. `execute` without a workdir didn't record the directory the process
ran in (`agent/agentproc`).
2. Refresh only rescanned repos already being watched, so a repo created
or recreated after its path was recorded stayed hidden
(`agent/agentgit`). A connection with no repos also got no first
message.
3. After a git watch reconnect, the new full state was merged into the
previous connection's map, so deleted repos stayed visible
(`useGitWatcher`).
4. A chat watch reconnect didn't refetch the open chat, so missed
`diff_status_change` events were lost (`AgentsPageLayout`).
5. Refresh didn't refetch the chat, whose diff statuses back the PR
tabs, and was disabled until git context existed (`GitPanel`,
`AgentChatPageView`).

Considered and deferred: a snapshot protocol with diffs fetched by hash,
which would keep large multi-repo diffs under the 4 MiB proxy read
limit, and making scans faster at scale. Neither is needed to fix
refresh, and together they made a single ~5900-line change that couldn't
be reviewed. The refresh spinner still runs for a fixed time, since
tying it to the response needs a request ID in the protocol.

</details>

---

_Generated by Coder Agents on behalf of @DanielleMaywood._
2026-10-14 17:04:40 +01:00
Danielle Maywood dad010ff11 fix(site/src/pages/AgentsPage): refetch the open chat when the chat watch reconnects (#30677)
Events missed while the chat watch socket was disconnected, such as
`diff_status_change`, only reach the open chat through a refetch. The
socket's `onOpen` handler already invalidated the chat lists, but not
the open chat, so its PR state could stay stale until a page reload.

`onOpen` now also invalidates the open chat entity and its diff
contents. This includes the first open, adding one refetch on page load,
the same as the existing chat list invalidation.

### Before


https://github.com/user-attachments/assets/b88a18d2-dfef-45bc-87f0-4c2d492b8ed7

### After


https://github.com/user-attachments/assets/65f84af2-7d2b-4a51-ae33-56cca62d9946

<details>
<summary>Decision log</summary>

The git panel's refresh button should show everything a page reload
would. Investigation found five ways it didn't, each fixed in its own PR
on the current `changes` protocol:

1. `execute` without a workdir didn't record the directory the process
ran in (`agent/agentproc`).
2. Refresh only rescanned repos already being watched, so a repo created
or recreated after its path was recorded stayed hidden
(`agent/agentgit`). A connection with no repos also got no first
message.
3. After a git watch reconnect, the new full state was merged into the
previous connection's map, so deleted repos stayed visible
(`useGitWatcher`).
4. A chat watch reconnect didn't refetch the open chat, so missed
`diff_status_change` events were lost (`AgentsPageLayout`).
5. Refresh didn't refetch the chat, whose diff statuses back the PR
tabs, and was disabled until git context existed (`GitPanel`,
`AgentChatPageView`).

Considered and deferred: a snapshot protocol with diffs fetched by hash,
which would keep large multi-repo diffs under the 4 MiB proxy read
limit, and making scans faster at scale. Neither is needed to fix
refresh, and together they made a single ~5900-line change that couldn't
be reviewed. The refresh spinner still runs for a fixed time, since
tying it to the response needs a request ID in the protocol.

</details>

---

_Generated by Coder Agents on behalf of @DanielleMaywood._
2026-10-14 17:04:39 +01:00
Danielle Maywood 63eef9efde fix(site/src/pages/AgentsPage/hooks): replace git state on reconnect (#30676)
When the git watch socket reconnected, the agent's full state was merged
into the map from the previous connection. Repos deleted while
disconnected, or lost because the agent restarted, stayed in the panel
until the page was reloaded.

The first `changes` message of each connection now replaces the map.
Later messages on the same connection are still applied as deltas. With
an agent that includes #30675, this also clears the panel when the new
connection has no repos.

### Before


https://github.com/user-attachments/assets/3df52312-8234-4c71-8e84-06dfabe8509c

### After


https://github.com/user-attachments/assets/2113a74c-f916-4ac4-bf11-1b58d78d1901

<details>
<summary>Decision log</summary>

The git panel's refresh button should show everything a page reload
would. Investigation found five ways it didn't, each fixed in its own PR
on the current `changes` protocol:

1. `execute` without a workdir didn't record the directory the process
ran in (`agent/agentproc`).
2. Refresh only rescanned repos already being watched, so a repo created
or recreated after its path was recorded stayed hidden
(`agent/agentgit`). A connection with no repos also got no first
message.
3. After a git watch reconnect, the new full state was merged into the
previous connection's map, so deleted repos stayed visible
(`useGitWatcher`).
4. A chat watch reconnect didn't refetch the open chat, so missed
`diff_status_change` events were lost (`AgentsPageLayout`).
5. Refresh didn't refetch the chat, whose diff statuses back the PR
tabs, and was disabled until git context existed (`GitPanel`,
`AgentChatPageView`).

Considered and deferred: a snapshot protocol with diffs fetched by hash,
which would keep large multi-repo diffs under the 4 MiB proxy read
limit, and making scans faster at scale. Neither is needed to fix
refresh, and together they made a single ~5900-line change that couldn't
be reviewed. The refresh spinner still runs for a fixed time, since
tying it to the response needs a request ID in the protocol.

</details>

---

_Generated by Coder Agents on behalf of @DanielleMaywood._
2026-10-14 17:04:39 +01:00
Garrett Delfosse 2b9fad8bba feat: add chat project sharing database layer (#30479)
Database and authorization layer for chat project sharing (experiment
`chat-projects`), part 1 of 2. #30439 adds the API on top of it. Nothing
here changes behavior: projects start with empty ACLs, no endpoint
writes them yet, and deleting a project still detaches its chats.
Project deletion is in #30683.

- **Schema:** migration `000621` adds `user_acl` and `group_acl` jsonb
columns to `chat_projects`, typed as `ChatACL` like chats, with GIN
indexes. It also adds the `chat_project:share` API key scope.
- **Queries:**
- `GetChatProjectsOwnedOrSharedWithUserID`: owned projects plus projects
with an ACL entry for the user directly, through a group, or through the
Everyone group (whose ID is the organization ID). Entries count only for
members of the project's organization. Keys match regardless of actions,
so dbauthz filters the rows.
- `IsChatProjectAccessibleByUserID`: whether a user owns a project or
holds a `read` or `*` grant on it, for code running under a system
subject. With sharing disabled, dbauthz answers owner-only.
- `GetChatProjectByIDForUpdate`: lets the ACL update read, modify, and
write in one transaction.
  - `UpdateChatProjectACLByID`.
- **RBAC:**
- New `share` action on `chat_project`. dbauthz requires it to update
the ACL.
- `ChatProject.RBACObject` attaches the project ACL. Memory objects get
a derived ACL: project `read` grants memory read, and `read` with
`update` grants memory create and delete. Wildcard grants count. Read
sharees change memories through their chats' agents, which #30439 gates.
Project instructions authorize through the same object, so a `read`
grant reads them and an `update` grant edits them.
- `--disable-chat-sharing` also covers projects: ACLs are ignored, the
ACL update is rejected, and roles deny `chat_project:share`. The help
text and chat sharing docs say so.
- **Audit:** `user_acl` and `group_acl` changes are tracked.
- **Tests:**
- Querier tests run the access queries both raw and through dbauthz
(owner, direct user, group, Everyone, wildcard grants, a member of
another org, grants without read).
- RBAC object and memory ACL tests, with an authorizer check, in both
sharing modes.
- dbauthz cases including the disabled guards, and the
`chat_project:share` negation in the roles test.

The generated TypeScript changes only add the new scope value.

> Generated by Coder Agents.
2026-10-14 10:42:00 -04:00
Jaayden Halko 85783e4275 feat(site/src/pages/AgentsPage): add a project picker to the new chat composer (#30455)
Adds a compact project picker next to the organization selector on the
main `/agents` composer, so a new chat can be filed in a project without
first opening that project's page. The picker only appears when the
`chat-projects` experiment is on, and the project page composer is
unchanged.

- The trigger reads "No project" or the selected project's icon and
name. The popover has a search box, a "No project" item noting that
chats can't be moved into a project later, the project list, and a
pinned "New project" footer.
- Only the composer's current organization's projects are listed,
because the server rejects a project from another organization. Changing
the organization clears the selection.
- Picking a project doesn't navigate, so the draft, attachments, model
and workspace stay as they are. The page already forwards `projectId`
from the form, so there are no page or API changes.
- The choice isn't remembered between visits. A chat can't move into a
project later, so a remembered choice could file chats in the wrong
project without the user noticing.
- "New project" opens the existing `ChatProjectDialog`, limited to the
current organization, and selects the new project when it is created.
The user stays in the composer.
- The new `CompactProjectSelector` gives each item its project ID as the
cmdk value and matches search only against the project name. Names
aren't unique, and cmdk's default scorer would otherwise also match the
search against the ID.

| Composer with both pickers | Long list with a pinned footer |
| --- | --- |
| ![Composer with the organization and project
pickers](https://raw.githubusercontent.com/coder/coder/assets/pr-30455-project-picker/pr/composer-picker-open.png)
| ![Project picker with many
projects](https://raw.githubusercontent.com/coder/coder/assets/pr-30455-project-picker/pr/picker-many-projects.png)
|

Tests cover picking a project, the default of no project, clearing the
selection on an organization change, selecting a project created from
the picker, name-only search, and Retry after a load error. Stories
cover the visual states: closed, open, loading, error, empty and a long
list.
2026-10-14 19:54:27 +07:00
Michael Suchacz 05fa806855 feat(site/src/pages/AgentsPage): move View PR into the git panel toolbar (#30751)
The View PR link moves from the branch row of the remote diff
(`RemoteDiffPanel`) to the git panel toolbar, between the view switcher
and the diff controls, and now uses the shared `Button`. It still shows
only when the selected ref has a PR (a `pr_number`, or a PR number
parsed from its URL), because a branch-only ref's URL can point at its
branch instead of a PR.

| Before | After |
| --- | --- |
| ![Before: View PR sits on the branch row below the PR
title](https://github.com/user-attachments/assets/378a08ed-7367-4656-9838-2d04f1f2f054)
| ![After: View PR sits in the toolbar next to the view
switcher](https://github.com/user-attachments/assets/306a5dca-a0bf-431e-82be-f2542162c17c)
|

At 360 px with PRs in two repos (new `NarrowPanelMultipleOrigins`
story), the switcher label truncates before View PR does:

![Narrow panel: the switcher label truncates and View PR stays
whole](https://github.com/user-attachments/assets/6ba2944a-1882-437c-88c3-9d37625534de)

Pixel baselines change for the GitPanel stories that show a PR. That is
expected.

Known tradeoff: View PR takes about 87 px of the toolbar row, so near
the 360 px minimum panel width, or with PRs in two repos, the switcher
label truncates with an ellipsis. The full label stays in the dropdown.
Shortening the label is out of scope here.

Follow-up: `GitPanel.tsx` was over the 500-line file guideline before
this PR (882 lines, now 900). Splitting it is a separate refactor.

## Testing

- `pnpm check`, `pnpm lint`, the GitPanel unit tests (9), and the
GitPanel story tests (19) pass.
- The per-ref test now checks that View PR links to the selected ref's
PR after a ref switch. It fails when the link uses the primary ref's
URL. The href is the link target, which no screenshot shows, so it stays
a Vitest assertion (FE1).
- A new test covers an older ref with no `pr_number` and a `/pull/` URL.
It fails when the gate reads only `pr_number`.

## Review record

- `frontend-review` (FE1 to FE10), `frontend-deep-review`, and
`coder-frontend-review` ran for four rounds, until no role had an open
finding.
- Fixed: the View PR markup moved into a `ViewPrButton` component with a
named props type, and the older-ref test was added.
- Kept: the href assertions (above); the badge wrapper in
`RemoteDiffPanel`, which right-aligns the diff stats; the existing
`selectedPrNumber` derivation, because the shared `prNumber()` helper
treats `pr_number` 0 differently; the `github.example.com` host in the
new story, where `coder-frontend-review` TS3 asks for `*.example.com`
but the file's older fixtures use `github.com`; and the file split
(follow-up above).

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-14 14:23:39 +02:00
Jaayden Halko 40c0adc7ac feat(site): design improvements for projects page (#30740)
<img width="1211" height="799" alt="Screenshot 2026-10-14 at 15 29 37"
src="https://github.com/user-attachments/assets/3a62b3e7-1e97-4d76-b643-9a6da9ae690d"
/>

1. Improves spacing and positioning of elements
2. Move meta data to details section
2026-10-14 16:40:40 +07:00
christin 01f78b37e3 fix(site/src): raise checkbox and radio border contrast to 3:1 (#30462)
The resting and hover borders of the shared `Checkbox` and
`RadioGroupItem` were below the 3:1 non-text contrast required by WCAG
1.4.11. In dark mode, the resting border was `#27272A` on `#09090B`
(1.33:1) and the hover border was `#3F3F46` (1.90:1). In light mode,
they were `#E4E4E7` on white (1.27:1) and `#A1A1AA` (2.56:1).

This adds two color tokens and uses them in both components:

| Token | Dark | Light |
|---|---|---|
| `border-control` | zinc-500 `#71717A` (4.1:1 on page, 3.7:1 on
`surface-secondary`) | zinc-500 `#71717A` (4.8:1 on white) |
| `border-control-hover` | zinc-400 `#A1A1AA` (7.8:1) | zinc-600
`#52525B` (7.7:1) |

A checked radio now uses the same `border-control` border as an
unchecked one. Before, it used `border-secondary`. Checked and
indeterminate checkboxes keep their inverted fill. `border-default` and
`border-secondary` are unchanged, so cards, dividers and other
decorative borders look the same. The colorblind themes inherit the new
tokens from the base light and dark blocks.

Fixes DEQ-583, DEQ-151, DEQ-426, DEQ-590, DEQ-433, DEQ-152, DEQ-300
Refs DEQ-285, DEQ-51

## Before and after

Checkboxes on Create user > Roles and radios on Appearance > Terminal
font. In the hover rows, the pointer is on the first item.

| | Before | After |
|---|---|---|
| Checkbox, dark, rest | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/assets/a11y-control-border-token/pr/before-checkbox-dark-rest.png"
/> | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/assets/a11y-control-border-token/pr/after-checkbox-dark-rest.png"
/> |
| Checkbox, dark, hover | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/assets/a11y-control-border-token/pr/before-checkbox-dark-hover.png"
/> | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/assets/a11y-control-border-token/pr/after-checkbox-dark-hover.png"
/> |
| Checkbox, light, rest | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/assets/a11y-control-border-token/pr/before-checkbox-light-rest.png"
/> | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/assets/a11y-control-border-token/pr/after-checkbox-light-rest.png"
/> |
| Checkbox, light, hover | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/assets/a11y-control-border-token/pr/before-checkbox-light-hover.png"
/> | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/assets/a11y-control-border-token/pr/after-checkbox-light-hover.png"
/> |
| Radio, dark, rest | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/assets/a11y-control-border-token/pr/before-radio-dark-rest.png"
/> | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/assets/a11y-control-border-token/pr/after-radio-dark-rest.png"
/> |
| Radio, dark, hover | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/assets/a11y-control-border-token/pr/before-radio-dark-hover.png"
/> | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/assets/a11y-control-border-token/pr/after-radio-dark-hover.png"
/> |
| Radio, light, rest | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/assets/a11y-control-border-token/pr/before-radio-light-rest.png"
/> | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/assets/a11y-control-border-token/pr/after-radio-light-rest.png"
/> |
| Radio, light, hover | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/assets/a11y-control-border-token/pr/before-radio-light-hover.png"
/> | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/assets/a11y-control-border-token/pr/after-radio-light-hover.png"
/> |

<details>
<summary>Decision log</summary>

- **New token instead of retuning `border-secondary`.**
`border-secondary` is used in 14 places, and only 4 are form controls.
The rest are card hovers, table-row outlines and chips. Retuning it
would make those heavier for no accessibility gain.
- **zinc-500 in dark mode.** zinc-600 is about 2.6:1 on the page and
2.3:1 on `surface-secondary`, so it fails. The darkest zinc-tinted value
that passes on both is about `hsl(240 5% 42%)`, but it only clears 3:1
by about 0.1, so we chose zinc-500 for margin.
- **Hover moves away from the background** (zinc-400 in dark mode,
zinc-600 in light mode). This way hover is never weaker than rest.
- **Scope is limited to `Checkbox` and `RadioGroupItem`.** These are
handled in follow-ups: inputs and field-style triggers (Input, Textarea,
Select, InputGroup, Combobox and others), the Switch (it uses
`surface-quaternary`), the template builder `ModuleCard` checkbox
(DEQ-148), the theme card radio indicator in `SingleModeSection` (the
theme-card part of DEQ-285) and the `ThemeSwatch` ring (DEQ-597).
- Text buttons and navbar menu buttons are exempt from 1.4.11 because
their text labels identify them, so they are not changed.

</details>

---

_Generated by Coder Agents on behalf of @chrifro._
2026-10-14 10:52:44 +02:00
Jaayden Halko 876378036e feat(site): show project instructions in the project page details panel (#30370)
Shows chat project instructions on the project page. Below the composer,
the page now has Chats and Details columns; the columns follow the page
width and stack on narrow screens. In Details, Instructions shows a
Create button when unset, or a three-line preview with a pen icon button
that opens the editor from the previous PR.

- The update and delete mutations write their result straight to the
cache. They cancel in-flight fetches first, so a refetch that read the
old value cannot overwrite the result.
- The instructions refetch on window focus, which is how the editor
learns about another user's change.
- The panel remounts per project, so an editor left open during
navigation can never save to the next project.

**What to scrutinize:** the cache handling in
`updateChatProjectInstructions` and `deleteChatProjectInstructions` in
`queries/chatProjects.ts`.

**Validation:** `ProjectDetailsPanel.test.tsx` asserts API calls and
cache state for create, edit, delete, save failure, error replacement, a
fast double click on Save, focus refetch, a refetch conflict resolved
with Keep my draft, stale refetches after Save and Delete, load retry,
and switching projects. Stories cover the view states. Dogfood UAT ran
on a remote Coder Agents workspace against the combined branch in #30349
and passed. It covered the UI flows, the API, and a real model following
instructions in new and existing chats.

## Screenshots

From UAT, except "Instructions set", which shows the current pen icon
design from Storybook.

| Empty state | Instructions set |
| --- | --- |
| <img width="720" alt="Details panel with no instructions and a Create
button"
src="https://raw.githubusercontent.com/coder/coder/assets/pr-30349-project-instructions/pr/empty-state.png"
/> | <img width="480" alt="Instructions preview clamped to three lines
with a pen icon edit button in the top right corner"
src="https://raw.githubusercontent.com/coder/coder/assets/pr-30349-project-instructions/pr/details-dark.png"
/> |

| Edit dialog | Concurrent edit warning |
| --- | --- |
| <img width="720" alt="Edit instructions dialog with Delete, Cancel,
and Save"
src="https://raw.githubusercontent.com/coder/coder/assets/pr-30349-project-instructions/pr/edit-dialog.png"
/> | <img width="720" alt="Warning that someone else changed the
instructions, with Keep my draft and Load latest"
src="https://raw.githubusercontent.com/coder/coder/assets/pr-30349-project-instructions/pr/conflict-warning.png"
/> |

<details>
<summary>Chat following project instructions</summary>

The instructions asked the model to end replies with PINEAPPLE, then
were edited to MANGO. The next turn of the same chat followed the edit.

<img width="720" alt="Chat where the first reply ends in PINEAPPLE and
the reply after the edit ends in MANGO"
src="https://raw.githubusercontent.com/coder/coder/assets/pr-30349-project-instructions/pr/chat-follows-instructions.png"
/>

</details>

---

Stack (merge top to bottom). #30377 through #29583 are f0ssel's split of
the original #29583, and this stack sits on #29583:
- #30377 data layer and project dialog (f0ssel)
- #30378 new-chat form locked to a project (f0ssel)
- #30379 project page route (f0ssel)
- #29583 sidebar folders (f0ssel), the base of this stack
- #30291 composer frame
- #30292 project chat list caches
- #30293 project page header
- #30294 project chats list
- #30367 instructions storage and prompt injection
- #30368 instructions API
- #30369 instructions editor dialog
- #30370 instructions Details panel 👈
2026-10-14 14:57:01 +07:00
Jake Howell a4c100e5fe fix(site): use singular pagination labels for one result (#30519)
> 🤖 This PR was written by Coder Agents on behalf of Jake Howell.

Fix pagination summaries such as “Showing 1 to 1 of 1 groups” and
“Showing 1 to 1 of 1 workspaces”. All nine callers now supply explicit
singular/plural labels, selected from the total result count rather than
the number of rows on the current page.

Preserves empty and filtered copy, loading states, capped counts, and
number formatting. Extends existing Storybook coverage for single
results and one-row final pages.

<details>
<summary>Implementation decisions and verification</summary>

- Follow the existing explicit-label and `=== 1` convention without
stripping suffixes or adding a dependency.
- Require both labels through the shared API; keep capped totals plural
because they are not exact counts.
- Passed 136 targeted Vitest tests, 109 Storybook tests, `pnpm format`,
`pnpm check`, full `pnpm lint`, and FE1–FE10 review.
- Browser-verified rendered wording. Screenshots were returned in chat;
direct image inspection was unavailable.

</details>
2026-10-14 14:26:12 +11:00
TJ 5d5f8672fa fix(site/src/modules/dashboard/Navbar/UserDropdown): announce new tab on version link (#30715)
The external link icon beside the build version in the user menu is
`aria-hidden`, so screen reader users are not told the link opens in a
new tab. This adds visually hidden ` (opens in new tab)` text to the
link, matching the existing pattern in `NavbarView.tsx`,
`SettingsHeader.tsx`, and `Markdown.tsx`.

`UserDropdownContent` is shared by the dashboard navbar (`UserDropdown`)
and the Agents sidebar (`UserSidebarFooter`), so this fixes both Deque
reports: DEQ-297 (Agents) and DEQ-526 (Header, marked duplicate). A unit
test asserts the link's accessible name includes the new-tab text.

<details>
<summary>Verification notes</summary>

- `UserDropdownContent.tsx` holds the only `SquareArrowOutUpRightIcon`
with `size-icon-xs` in `site/src`, matching both audit selectors.
- The other version link (prerelease badge in `NavbarView.tsx`) has no
icon.
- The new test fails without the fix and passes with it; Biome and `tsc`
are clean.
- Not verified with NVDA in a browser.

</details>

> 🤖 Generated by Coder Agents on behalf of @tracyjohnsonux.
2026-10-13 19:51:02 -07:00
TJ 0e9edc1eb0 fix(site/src/pages/AgentsPage/components): put context usage popover ARIA on the button (#30708)
Fixes
[DEQ-64](https://linear.app/codercom/issue/DEQ-64/elements-must-only-use-supported-aria-attributes)
(axe `aria-allowed-attr`, WCAG 4.1.2).

On desktop, the context usage ring wrapped its `<button>` in a `<div>`
inside `<PopoverTrigger asChild>`, so Radix put `type="button"`,
`aria-haspopup`, `aria-expanded`, and `aria-controls` on a role-less
`div`. The hover wrapper now sits outside the trigger, so the popover
attributes land on the button. Hover-to-open behavior is unchanged.

The `ResourceIssue` story now asserts the button gets
`aria-expanded="true"` on hover. It fails without the fix.

<details>
<summary>Scan for other occurrences</summary>

- **axe via Storybook Vitest:** temporarily enabled
`@storybook/addon-a11y` with `test: "error"` and `runOnly:
["aria-allowed-attr"]`, then ran all stories under `pages/AgentsPage`,
`pages/AISettingsPage`, and `modules/aiModels` with `--bail=0` (118
files, 1661 stories, checked after play functions). Before the fix it
flagged only this `div` (4 `ContextUsageIndicator` stories). After the
fix: 0 violations. The temporary config is not part of this PR.
- **Static:** no other
Popover/DropdownMenu/Collapsible/Dialog/HoverCard/Select trigger in
`site/src` uses `asChild` on a non-button element. Every manual
`aria-expanded`/`selected`/`pressed`/`checked`/etc. in the scanned
folders is on an element whose role supports it.

Note: `.storybook/vitest.setup.ts` calls `setProjectAnnotations` itself,
which makes `@storybook/addon-vitest` skip auto-provisioning addon
annotations, so the a11y addon never runs axe in story tests today.
Enabling it repo-wide is a separate change.

</details>

---

_Generated by Coder Agents on behalf of @tracyjohnsonux._
2026-10-13 19:50:03 -07:00
a46d514a23 feat(site): add the project instructions editor dialog (#30369)
Adds `ProjectInstructionsDialog`, the dialog for creating, editing, and
deleting a chat project's instructions. The Details panel in the next PR
opens it.

- Save is disabled when the draft is blank or unchanged once normalized
the way the server stores it. `sanitizePromptText` mirrors
`codersdk.SanitizePromptText`: it strips invisible characters, trims
trailing whitespace on each line, and collapses extra blank lines.
- If the saved instructions change while the dialog is open (another
editor saved them), a warning appears. Save and Delete stay disabled
until the user picks Load latest or Keep my draft. The check only
applies while the dialog is open, so the user's own save does not flash
the warning during the close animation.
- An error without an API message is labelled by the action that failed
("Failed to save instructions." or "Failed to delete instructions.").

**What to scrutinize:** the conflict handling in
`ProjectInstructionsForm`, which keeps the instructions the draft
started from as its baseline.

**Validation:** `ProjectInstructionsDialog.test.tsx` asserts the
`onSave`/`onDelete` callbacks for blank drafts, edits the server would
discard, and conflicts. `invisibleUnicode.test.ts` checks
`sanitizePromptText` against cases verified with the Go function.
Stories cover the create, edit, error, error-without-message,
invisible-character warning, saving, deleting, and conflict states.
`pnpm exec tsc`, biome, oxlint, and knip pass.

---

Stack (merge top to bottom). #30377 through #29583 are f0ssel's split of
the original #29583, and this stack sits on #29583:
- #30377 data layer and project dialog (f0ssel)
- #30378 new-chat form locked to a project (f0ssel)
- #30379 project page route (f0ssel)
- #29583 sidebar folders (f0ssel), the base of this stack
- #30291 composer frame
- #30292 project chat list caches
- #30293 project page header
- #30294 project chats list
- #30367 instructions storage and prompt injection
- #30368 instructions API
- #30369 instructions editor dialog 👈
- #30370 instructions Details panel

---------

Co-authored-by: Garrett Delfosse <delfossegarrett@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 09:29:28 +07:00
McKayla はな 893461a91f fix: allow fixing stale parameters when the active version is required (#30660) 2026-10-13 17:25:42 -06:00
Michael Suchacz fa7cd62c8b feat: send RFC 8707 resource parameter on MCP server OAuth2 flows (#30651)
Coder's MCP OAuth2 client never sent the RFC 8707 `resource` parameter,
which the MCP authorization spec requires on authorization and token
requests. Authorization servers that enforce it cannot be connected:
with Dagster+, discovery and dynamic client registration succeed, then
the authorize request fails with "MCP OAuth requests must include
resource".

MCP server configs get an `oauth2_resource` field (migration 000618,
admin-only in API responses, audited). Coder sends it on the authorize
redirect, the authorization-code exchange, and token refresh. Refresh
now goes through `clientcredentials` with `grant_type=refresh_token`,
because `oauth2.Config` cannot add parameters to refresh requests; it
keeps the same auth-style detection and error types, and adds no
dependency.

Auto-discovery fills the field with the most specific Protected Resource
Metadata `resource` that covers the server URL, stored as advertised. As
in the MCP SDKs, a resource covers URLs on its origin at or under its
path, so `/mcp` covers `/mcp/` but `/mcp/` does not cover `/mcp`. URLs
compare in RFC 3986 normal form (default ports, percent-encodings, dot
segments, and an empty path as `/`), ports ignore leading zeros, IPv6
literals compare by address, and `%2F` stays inside its segment. Unlike
the SDKs, a resource with a query, even an empty one, only covers a
server URL with the same query. When none covers it, Coder uses the
server URL with the scheme and host lowercased and user information and
fragment removed, or sends no resource when that is not a valid resource
indicator. An explicit value in the create request wins.

Manual configurations send no `resource` unless an admin sets one,
because some providers reject it (Microsoft Entra ID v2 fails with
AADSTS901002). Values must be absolute RFC 3986 URIs without a fragment,
user information, or an IPv6 zone identifier.

The resource binds grants like the other OAuth settings: changing it
deletes stored user tokens, and a callback whose config resource changed
after the user started connecting returns 409 before the code is
redeemed. The connect step records the resource in a callback-scoped
cookie for that check.

Discovery only runs when a configuration is created, so existing
auto-discovered configurations, including the Dagster+ one that prompted
this, keep an empty resource until an admin sets the new "Resource URL"
field or re-creates the server.

![MCP server edit form showing the Resource URL field filled by
auto-discovery](https://github.com/user-attachments/assets/319bc8a0-f6e1-4b7d-8878-8e341c94e590)

## Testing

- Go: discovery (most specific match, path and trailing-slash
boundaries, query matching including empty queries, equivalent ports,
IPv6 literals, escapes, and dot segments including escaped ones, the
normalized fallback and an invalid one, explicit override), validation
(URI characters, brackets, IP literals, user information, trimming),
grant deletion on change, clearing on an auth type change, redaction for
members, refresh with and without a resource, the 409 when the resource
changes mid-flow (the authorization server never receives the code), and
a connect and callback flow against an authorization server that rejects
requests without the resource. Each new test was checked to fail when
the behavior it covers is broken, except two malformed IP literal hosts
that Go's URL parser already rejects.
- Vitest: the form round-trips the stored value, and clearing the field
sends `""` on update.
- Remote UAT through Coder Agents
([chat](https://dogfood.cdr.dev/agents/58695d5b-2112-400b-88f7-ab64c678f756))
on a test deployment with loopback MCP and authorization server mocks:
- Round 1 on 2b875068e0: literal verdict FAIL. When the resource
changed between connect and callback, the callback redeemed the code for
the new resource and stored a grant. The 409 binding fixes it.
- Round 2 on 4b54ffb5f9: PASS. The mid-flow change returned 409 with no
token request in every variant (strict and lenient servers, set,
cleared, abandoned flows, parallel configs); auto-discovery, connect,
refresh, validation, and the docs matched. Minor observations: the
binding cookie is an unkeyed digest, so a member who edits their own
HttpOnly cookie can redeem a code with the current resource, which a
fresh connect also gives them; other settings changed mid-flow still
reach the token endpoint, as on main; the 409 popup shows raw JSON; the
UI does not warn that a resource change disconnects users; validation
toasts omit the server's detail.
- Round 3 on the final head 2d6abfac49: PASS. The head moved during the
round, so it ran in four parts, the last on 2d6abfac49. That part
covered the matching rules (queries including empty ones, default and
zero-padded ports, `%2F`, escaped dot segments, trailing slashes, IPv6
literals), the invalid fallback (stored empty, no `resource` sent),
every validation value through the API and the UI, and the discovery,
connect, refresh, and mid-flow 409 flows. The equivalent-encoding cases
last ran live on 5c23fe00dca; unit tests cover them on the final head.
Observation: the path-aware metadata URL is built from the decoded path,
so `/mcp%2Ftenant` fetches metadata for `/mcp/tenant`, as on main.
- `frontend-review` and the eight-role `coder-frontend-review` ran on
the `site/` diff; every role passes after the label, help text, row
layout, API doc wording, and form test fixes. Declined: linking field
descriptions with `aria-describedby`, because the form's shared `Field`
primitive renders every description without one, so it belongs in a
change to all fields.

## Review record

| Review | Commit | Findings | Disposition |
|---|---|---|---|
| [Codex round
1](https://github.com/coder/coder/pull/30651#pullrequestreview-5492085359)
| 4b54ffb5f9 | P2: a resource with another query was treated as
covering the server URL | Reproduced with a failing test, fixed in
bf0879560c, resolved |
| [Codex round
2](https://github.com/coder/coder/pull/30651#pullrequestreview-5492305363)
| bf0879560c | P2: explicit default ports; P2: `%2F` treated as a
segment boundary; P2: `url.Parse` accepts spaces and invalid escapes |
Each reproduced with a failing test, fixed in 16a1d783fa, resolved |
| [Codex round
3](https://github.com/coder/coder/pull/30651#pullrequestreview-5492677517)
| 16a1d783fa | P1: docs wrapped mid-sentence (DOC1); P2: equivalent
percent-encodings compared unequal; P2: brackets accepted outside an IP
literal host | Fixed in 8d617f0528 (the P2s reproduced with failing
tests; the same pass also covers the query, dot segment, and user
information cases), resolved |
| [Codex round
4](https://github.com/coder/coder/pull/30651#pullrequestreview-5493033885)
| 8d617f0528 | P2: the server URL fallback was not validated; P2:
escaped dot segments were compared before decoding | Each reproduced
with a failing test, fixed in 5c23fe00dc, resolved |
| [Codex round
5](https://github.com/coder/coder/pull/30651#pullrequestreview-5493342889)
| 5c23fe00dc | P2: an empty query (`?`) was treated as no query |
Reproduced with a failing test, fixed in 72c019ff5e, resolved |
| [Codex round
6](https://github.com/coder/coder/pull/30651#pullrequestreview-5493524846)
| 72c019ff5e | P2: `/mcp/` covered `/mcp`; P2: equivalent IPv6 literals
compared unequal; P2: malformed IP literals accepted | The first two
reproduced with failing tests. Go 1.26.5 `url.Parse` already rejects the
hosts named in the third, but it accepted an IPv6 zone identifier. All
fixed in ef2df2c4e7, resolved |
| [Codex round
7](https://github.com/coder/coder/pull/30651#pullrequestreview-5493668131)
| ef2df2c4e7 | P2: leading zeros in a port made equal origins differ |
Reproduced with a failing test, fixed in 2d6abfac49, resolved |
| [Codex round
8](https://github.com/coder/coder/pull/30651#issuecomment-6128743972) |
2d6abfac49 | None: "Didn't find any major issues" | Clean verdict on
the current head |

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-13 23:03:08 +02:00
Michael Suchacz 0702372dcf chore: add frontend-deep-review agent skill (#30182)
Adds a `frontend-deep-review` agent skill and tells agents to run it and
fix its findings whenever they modify frontend code.

The skill reviews a `site/` diff with eight parallel read-only reviewer
roles (simplicity, comments, tests and stories, React patterns, reuse,
styling and a11y, types and naming, data flow). Each role owns numbered
checkpoints distilled from 671 human inline review comments on `site/`
PRs between April and September 2026. It complements the existing
`frontend-review` FE1 to FE10 gate rather than replacing it. Where a
checkpoint conflicts with the root `AGENTS.md`, `site/AGENTS.md`, or
`FRONTEND_PATTERNS.md`, the repository rule wins and the reviewer
reports the conflict. To review someone else's PR, the skill does not
check out the branch, so the author's Git hooks never run. Instead it
pins the head and base SHAs, reads the PR only as Git objects, reads the
rules at the base branch tip, and treats the PR's text and file names as
data.

Changes:

- `.claude/skills/frontend-deep-review/`: `SKILL.md`,
`reviewer-prompt.md`, and eight `roles/*.md` checkpoint files, symlinked
from `.agents/skills/` like `frontend-review`.
- `AGENTS.md` and `site/AGENTS.md`: run the skill for frontend changes
and fix `blocking` and `should-fix` findings before handoff.
- `.claude/skills/frontend-review/SKILL.md`: points to the new skill
after the FE audit passes.

The published version drops reviewer names, verbatim quoted comments, PR
references, per-checkpoint comment counts, and the raw comment dataset
from the internal original, keeping only the checkpoints and their
fixes. The helpers, files, and lint rules a checkpoint recommends or
cites as a correct example exist on `main`. Identifiers inside a
checkpoint's examples of what to flag come from past reviews, and some
have since been renamed or removed; `SKILL.md` and the reviewer prompt
tell reviewers to match the pattern, not the name.

Testing: `make lint/agents lint/markdown lint/emdash`,
`markdownlint-cli2` on the skill files, and the pre-commit hook pass.
Every file path and identifier the skill cites was checked against
`origin/main` with `git grep`; the only misses are those past-review
example names and placeholders such as `mockX`.

> Xum opened this PR on behalf of Mike.
2026-10-13 22:53:38 +02:00
Michael Suchacz adcce66826 feat: load organization skills in chats (#30613)
## Stack

Closes
[CODAGT-1341](https://linear.app/codercom/issue/CODAGT-1341/support-organization-level-skills-in-coder-agents).
This stack adds organization skills to Coder Agents: admins publish a
skill once per organization, share it with groups or members, and chats
in that organization load it next to personal and workspace skills. It
replaces the 21-PR draft stack #30584 to #30604 with five PRs, bottom to
top:

1. #30609 data model: one migration, queries, RBAC and dbauthz
2. #30610 organization skill API routes and the scope-neutral SDK
3. #30611 sharing ACL routes
4. #30612 settings UI: shared skills table, admin page, member section
5. **#30613** chats: chatd loading, `/` menu, docs (this PR)

## What changes

This PR makes chats use organization skills: chatd adds the skills
shared with the chat owner in the chat's organization to the skill
index, `read_skill` loads them, the `/` menu offers them, and the docs
describe the feature.

**chatd** (`coderd/x/skills`, `coderd/x/chatd`):

- New source `skills.SourceOrganization` (`"org"`).
`MergeSkills(personal, organization, workspace)` sorts by name, then
source. A name in one source keeps its bare alias; a name in more than
one source gets a qualified alias in every source that has it
(`personal/<name>`, `org/<name>`, `workspace/<name>`), so no source
overrides another. `Lookup` returns `ErrSkillAmbiguous` for a bare name
that matches several sources.
- **Authorization (please review closely):** each turn, chatd reads
organization skill metadata and bodies as the chat owner's real RBAC
subject with their roles and groups (`chatOwnerSubject`), never as the
chatd system actor, so per-skill ACLs apply. Disabled skills are
skipped. A skill that is deleted, disabled, or no longer shared by the
time `read_skill` runs is reported as not found. If the owner subject or
the metadata can't be loaded, turn preparation fails with the error, as
main now does for personal skills (#30492), instead of running the turn
without organization skills.
- `read_skill` loads personal and organization bodies through one
`LoadStoredSkillBody` callback. An index description longer than 1,024
runes, the Agent Skills specification limit
(`maxSkillIndexDescriptionRunes`), is cut to 1,023 runes plus an
ellipsis so it stays within the limit; `read_skill` still returns the
full skill.
- Parse and lookup errors keep `%w` last on purpose: with `xerrors`, a
`%w` that isn't last repeats the cause, so main's detail read "skill is
too large: got N bytes, maximum is 65536 bytes: skill is too large".
This stack returns the cause once (ThomasK33 asked about the order in
review).

**`/` menu** (`site/src/pages/AgentsPage`): the chat page fetches the
chat organization's enabled skills (`organizationSkills(...)` from
#30612's queries) next to personal ones. `utils/skillAliases.ts`
(`resolveSkillTriggers`) follows `MergeSkills`: a personal or
organization trigger is bare unless its name is in more than one source,
then it becomes `/personal/<name>` or `/org/<name>`. While any list is
unknown every trigger is qualified, because a qualified alias always
resolves; workspace triggers are always qualified. Built-in `/clear` and
`/compact` stay pending until every skill list is known, and a list with
no data counts as unknown even after an error, so a failed fetch can't
make a built-in run in place of a same-named skill. Disabled skills
never shadow a built-in.

**Docs:** `docs/ai-coder/agents/extending-agents.md` gains two sections,
Organization skills (manage them, control who can use one, find the ones
you can use) and How chats load stored skills (turning a skill on or
off, skills with the same name). The admin page's organization picker
shows only for a user who can view more than one organization's skills.
The member "From your organizations" section shows one organization at a
time with its own picker, starts on the default organization, and has
Export all for the selected organization. The Enabled column is a switch
for editors and a check or X otherwise. The settings table in
`platform-controls/organizations.md` gains rows for organization and
personal skills.

## Commits

The first commit reproduces the reviewed chatd code from the old stack;
the site and docs commits reapply the old layers on #30612's queries,
and the rest are labeled trims or fixes.

- `aa634eec94a` feat(coderd/x): load organization skills in chats.
Reproduction of the old skill sources and chatd layers (old head
`a1047f4986`).
- `97945db4e86` test(coderd/x/skills): make `TestMergeSkills` (6 cases)
and `TestLookup` (8 cases) table-driven; coverage is 100% before and
after, with the same blocks.
- `dde61cc5fe2` test(coderd/x/chatd): drop skill tests duplicated by the
organization skills integration test
`TestOrganizationSkillsFollowChatOwnerAccess`; the covered block set of
the touched code is identical (348 of 506 blocks).
- `aad14b2dd05` feat(site/src/pages/AgentsPage): show organization
skills in the slash menu. The old alias helper and slash menu layers,
applied cleanly on #30612's queries.
- `712e272e140` test(site/src/pages/AgentsPage): trim duplicated slash
menu tests and stories. Deletes `AgentChatPage.test.tsx` (177 lines; its
assertions live in `submitChatTurn.test.ts` and
`ChatMessageInput.test.tsx`), two duplicate `ChatMessageInput` cases,
six of 17 `SkillsTriggerMenu` stories, and one disabled-skill fixture
row from a `ChatMessageInput` story.
- `232535e1011` docs(docs/ai-coder/agents): document organization
skills. From the old docs layer, with the View line updated for #30612's
read-only dialog and Download.
- `62dbbd2dfd3` refactor(site/src/pages/AgentsPage): address frontend
review on the slash menu.
- `7349eac4389` Merge the updated settings-ui layer into chats. Brings
in the #30610 and #30612 fixes; the merge has no changes of its own.
- `0f84a30ec69` Merge the updated settings-ui layer into chats again,
for #30612's read-only View fix; no changes of its own.
- `fb37822f985` fix(coderd/x/chatd/chattool): keep truncated skill index
descriptions within 1,024 runes. The cut now happens at 1,023 runes so
the ellipsis fits; the existing `skill_test.go` assertion now expects
1,023 runes plus the ellipsis.
- `1543666b584` docs(docs/ai-coder/agents): state the 1,023-character
cut before the skill index ellipsis.
- `96f1da47c9a` and `13f03d095ff` merge the updated settings-ui layer
for #30612's cache and Skills link fixes; no changes of their own.
- `b03c14c86de` and `41cc078b85a` merge the updated settings-ui layer;
`b03c14c86de` moves the `AgentChatPage.tsx` and `ChatMessageInput.tsx`
call sites, and their tests and stories, to #30612's split
`organizationSkills(...)` query.
- `1a32d05c9bc`
test(site/src/pages/AgentsPage/components/ChatMessageInput): restore
per-source loading stories for the slash menu.
- `830b0466982` fix(coderd/x/chatd): fail turn preparation when
organization skills cannot load. Main now returns database errors from
turn preparation (#30492); organization skills still logged and dropped
the failure, so a failed read silently hid them from the model. The new
`TestFetchOrganizationSkillMetadataListFailure` covers the returned
error.
- `84c135f8801` and `a6f8f63ad13` merge the updated settings-ui layer,
which brings in main; they resolve `generation_preparer.go` against
main's turn preparation error handling.
- `19b7880840f` merges #30612's member page test change; no changes of
its own.
- `c74d597f3c5` merges #30612's read-only status change; no changes of
its own.
- `f89c232c081` merges #30612's skills test changes; no changes of its
own.
- `17d5b9bfbc5` docs(docs/ai-coder/agents): describe the member skills
organization picker and read-only Enabled status.
- `f97b1710f71` merges the updated settings-ui layer (main at
`e7b76872098`); no changes of its own.

## Size

Production +558/-213 (docs +116/-6), tests +895/-262; total +1453/-475.
This is above the 500-line reviewer-friendly limit on purpose: Mike
asked for 4 to 5 PRs instead of the 21 small ones this stack replaces.

## Replaces

#30593 (skill sources), #30594 (chatd), #30601 (alias helper), #30602
(slash menu), #30604 (docs).

## Validation

- Go: all 21 packages under `coderd/x/skills` and `coderd/x/chatd` pass
on `62dbbd2dfd3`, plus the coverage comparisons above;
`coderd/x/chatd/chattool` passes again on `13f03d095ff` after the
truncation fix.
- Site: `pnpm run lint` and Biome clean; Vitest 102 tests; Storybook
Vitest 49 + 4 tests. `pnpm run lint` reran clean on `13f03d095ff`.
- On `a6f8f63ad13`, this PR's head before the test-only `19b7880840f`
merge: `go test` passes for `coderd/x/skills`,
`coderd/x/chatd/chattool`, and the `coderd/x/chatd` skill tests (`-run
'OrganizationSkill|Skill'`); 38 Vitest files covering skills and the
slash menu pass, 542 tests with 2 skipped.
- Docs: markdownlint 0 findings, Vale 0 findings.
- On `17d5b9bfbc5`: Vale clean, markdownlint and `make pre-commit` pass.
On `f97b1710f71`: `go build`, the `coderd/x/skills` and `chattool`
tests, `pnpm check` and `tsc` pass.
- Remote UAT round 7 through Coder Agents: PASS
([chat](https://dogfood.cdr.dev/agents/3426d4d3-2139-4ae1-8e0c-72ddeff413a8)).
- Full run on `62dbbd2dfd3`, including an in-place upgrade from main
`50c785cc6d7`: personal skills kept their IDs, names, content hashes and
timestamps, came up enabled, and worked in chats (`read_skill`) and the
`/` menu; create, edit, download and delete worked after the upgrade.
The admin page, sharing, the Enabled toggle, the member section, the `/`
menu, chats reading organization skills as the owner, limits, audit,
authorization and scoped keys, and the docs all passed.
- Delta run on `7349eac4389`: an update-only key gets 404 on PATCH with
no content in the response; saving permissions refreshes Updated without
a reload; smoke of the admin page, auditor View and Download, the `/`
menu and `read_skill`.
- Delta run on `0f84a30ec69` for `aaa356f0fd5`: read-only View, as an
org auditor and as a member, shows no Name hint, focuses no field and
starts on Close, and Escape or Close returns focus to the row menu
button; Add skill and Edit still show the hint and focus the import
field, and a saved edit persisted; smoke of the admin page, the `/` menu
organization group and `read_skill`. Screen-reader behavior was not
tested.
- Delta run on `13f03d095ff` for `698dd83946d`, `f82573cd1c2`,
`0e55137de7b` and `fb37822f985`: for an organization admin of org B who
is a plain member of org A, the sidebar Skills link drops `?org=` for
org A on seven AI settings pages and opens org B's skills, keeps `?org=`
for org B, and a typed `/ai/settings/skills?org=` for org A still shows
the permission error. A skill deleted and recreated with the same name
loads a fresh sharing list showing only Everyone, with no stale grant. A
1,500-character description appears in the chat's skill index as 1,023
characters plus an ellipsis, while `read_skill` returns all of it. Smoke
of the admin toggle, the member section, the `/` menu organization group
and `read_skill`.
- Low findings: the read-only View hint and field focus, fixed in
`aaa356f0fd5` and verified by the delta run above. The member section's
always-on disabled Enabled switch (also in the old stack) is replaced by
a read-only status icon in #30612's `63bb42754e3`, which has no UAT run.
Not fixed: `/` shows the `/personal/` prefix while organization skills
fail to load (the unknown-list rule above); the shared Switch
component's disabled styling (unchanged from main). A user who can read
skills in only one organization sees no organization name on the Skills
page, because the picker is hidden for a single organization, as on the
MCP servers page on main.
- Not tested: the workspace skills group (no running agent on the test
deployment), the 100-per-user personal cap, and behavior without a
Premium license.
- Remote UAT round 8 through Coder Agents: PASS on `b03c14c86de` (same
chat). The remote grader marked the unknown `read_skill` item FAIL only
because the prompt asked for the literal phrase "skill not found"; the
result names the skill once and says "not found" once, the same format
as main, so the runner graded it PASS on intent.
- Member page: starts on the default organization, shows one
organization's table with the read-only note once, Export all downloads
only the selected organization's skills, the picker is hidden with one
organization, and an organization without enabled skills isn't listed.
- Personal and admin tables: create, edit, toggle and delete each
refetch the list; reopening the editor shows the new content even with
the detail request delayed; a toggle changes state once, with no
flicker. A skill deleted and recreated with the same name shows only
Everyone in sharing.
- The sidebar Skills link keeps `?org=` only for an organization whose
skills the user can read. A create over 64 KiB returns 400 with the
detail "got 70000 bytes, maximum is 65536 bytes: skill is too large",
the cause once. The `/` menu shows Commands, Personal skills and
Organization skills, and invoking an organization skill loads its body
through `read_skill`.
- New low findings, not fixed: the member page picker's listbox is
labeled "Suggestions" (likely from the shared
`OrganizationAutocomplete`); a typed body over 64 KiB shows only the
size counter and a disabled Create until Import; `/ai/settings?org=`
redirects to the models page without `?org=`. The first and third were
not checked against main.
- `9002dc60382`, `1a32d05c9bc`, `830b0466982`, `ac3e62c83b7`,
`63bb42754e3`, `82b2487ad17`, `17d5b9bfbc5` and the main merges came
after `b03c14c86de`; they have tests but no UAT run.
- Every UAT round ran the migration as `000615_skills`. The heads ship
the same up and down SQL as `000621_skills`, so the upgrade path from
main's 000620 to 000621 has no UAT run.

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-13 17:13:36 +00:00
Michael Suchacz 6498c5776d feat(site): add organization skill settings with a shared skills UI (#30612)
## Stack

Part of
[CODAGT-1341](https://linear.app/codercom/issue/CODAGT-1341/support-organization-level-skills-in-coder-agents).
This stack adds organization skills to Coder Agents: admins publish a
skill once per organization, share it with groups or members, and chats
in that organization load it next to personal and workspace skills. It
replaces the 21-PR draft stack #30584 to #30604 with five PRs, bottom to
top:

1. #30609 data model: one migration, queries, RBAC and dbauthz
2. #30610 organization skill API routes and the scope-neutral SDK
3. #30611 sharing ACL routes
4. **#30612** settings UI: shared skills table, admin page, member
section (this PR)
5. #30613 chats: chatd loading, `/` menu, docs

## What changes

This PR turns the personal skills page into a skills table that works
for any owner, then uses it for an organization admin page and a
read-only member section. It changes only `site/`.

**Shared skills table.** The personal page's files move to owner-neutral
paths (kept as git renames, so the diff shows edits):
`AgentSettingsPersonalSkillsPageView.tsx` becomes
`components/SkillsTableView.tsx`, `AgentSettingsPersonalSkillsPage.tsx`
becomes `components/SkillsTable.tsx`, `PersonalSkillEditor` becomes
`SkillEditor`, and `utils/personalSkills` becomes `utils/skills`.
`queries/userSkills.ts` keeps main's query names and keys for personal
skills, and the new `queries/organizationSkills.ts` holds the
organization and ACL queries. The table takes a `SkillsScope` (list,
detail, create, update and delete queries), built by `userSkillsScope`
or `organizationSkillsScope` in `utils/skills.ts`, plus an `access`
object (`create`, `update`, `delete`) and its copy as props.

- **Enabled toggle:** `SkillEnabledSwitch` runs one update mutation per
row from the table's scope, is disabled while it is pending, and
invalidates the list when it settles, which also drops a skill deleted
elsewhere.
- **Read-only status:** a table without update access (the member
section, and auditors on the admin page) shows a green check ("Enabled")
or a gray cross ("Disabled") in the Enabled column instead of a disabled
switch. Editable tables keep the switch.
- **Read-only view:** when the caller can't update a skill, the row
opens `SkillEditor` in read-only mode with a Close button, replacing the
old View dialog. It omits the naming hint and starts focus on Close.
- Dialogs return focus to the control that opened them
(`useDialogFocusReturn`).
- Create, update and delete invalidate the skill list instead of
patching it by hand. Update also writes the PATCH response to the
skill's detail, so a reopened editor shows the new content.
- Saving an ACL refreshes the organization skill list and the skill's
detail. Deleting a skill removes its detail and the ACL queries cached
under it, so a skill recreated with the same name doesn't inherit them.

**Admin page** at `/ai/settings/skills?org=<name>`
(`AISettingsPage/SkillsPage`): lists, creates, edits, toggles and
deletes organization skills with the caller's per-organization
permissions, and opens `OrganizationSkillSharingDialog` for users with
share. A requested organization the caller can't read shows the
permission error instead of falling back to another organization, so a
skill is never added to an organization the URL didn't name. The new
`viewAnyOrganizationSkills` permission adds Skills to the AI settings
sidebar, shows the navbar's AI settings link, and lets the AI settings
index redirect to the skills page. The sidebar's Skills link keeps the
current `?org=` only when the user can read that organization's skills,
using the page's own filter (`readableSkillsOrganizations`), so
following the link never lands on the permission error.

**Member section.** The agent settings page becomes
`/agents/settings/skills` ("Skills" in the sidebar; the old
`/agents/settings/personal-skills` URL redirects). Below personal
skills, "From your organizations" shows one organization at a time,
read-only. Its picker lists the organizations the user is a member of
that have enabled skills for them (an organization still loading or
whose list failed stays listed), starts on the default organization, and
is hidden when only one organization qualifies. Export all downloads
only the selected organization's skills. If the selected organization's
list fails, a retry button (labeled "Retry loading _name_ skills")
replaces the table.

**Shared sharing helpers.** `ChatModelPrincipalAutocomplete` becomes
`components/ACLPrincipalAutocomplete`, used by the model, MCP server and
skill sharing dialogs; `MCPServerPrincipalAutocomplete` is deleted and
the common ACL diff logic moves to `aclSharing.ts`. A new
`ChatModelSharingDialog.test.tsx` guards the model dialog through the
swap.

Declined on purpose (Mike's call): extra toggle cache hardening, namely
a pending-toggle map, reconciling a View that 404s, and guarding a stale
refetch against a retried toggle. Invalidating on settle already
refetches the list after every toggle.

## Commits

This PR reworks the old site layers rather than reproducing them. The
commits go in review order: shared helpers, renames, the shared table,
the two pages, then review fixes.

1. `6463992b717` refactor: share the ACL principal autocomplete and
sharing helpers.
2. `e18599f5049` refactor: move the personal skills files to shared
skills paths (file moves plus import path updates only).
3. `3c71e6c2168` refactor: drop the personal prefix from shared skill
symbols.
4. `3be35bcde68` refactor: key the skills table and queries by owner.
5. `d0c1dfcfdd1` fix: return focus to the opener when a skills dialog
closes.
6. `92ef6351eb4` feat: add an Enabled toggle to the skills table.
7. `31c103e5396` refactor: move the skill edit and delete dialogs to
their own file.
8. `cff0bef4576` feat: view a skill read-only when the user cannot edit
it.
9. `a8b54a22981` feat: add the organization skills admin page with
sharing.
10. `391401b62b7` feat: show organization skills on the personal skills
page.
11. `2cf6e250a60` refactor: keep the skill dialogs in the table view and
extract focus return. This undoes the file split from commit 7 so
`SkillsTableView.tsx` still diffs as a rename (497 lines then, 507 at
the head).
12. `0af93452c7d` test: keep only the role wiring test for the skills
page.
13. `8032c6708a8` fix: refetch the skill list after a create when no
list is cached (superseded by item 22).
14. `a52f9ba659c` refactor: tidy skills copy, editor prop naming, and
story fixtures.
15. `f10bc316305` fix: refresh the organization skill list after saving
its ACL. Saving an ACL bumps the skill's `updated_at`, so the save now
also invalidates the organization skill list and the skill's detail; a
new test in `api/queries/skills.test.ts` (now
`organizationSkills.test.ts`) covers it.
16. `698a6d0b55f` Merge the updated sharing layer into settings-ui,
bringing in #30610's PATCH fix.
17. `aaa356f0fd5` fix: drop the editing hint and field focus from the
read-only skill view. From the remote UAT: read-only View no longer
shows the Name field's naming hint, and the dialog opens with focus on
Close instead of a field (`onOpenAutoFocus`, like `ChatSearchDialog`).
The new `SkillEditor.test.tsx` case "starts focus on Close when
read-only" fails without the fix.
18. `698dd83946d` fix: drop a deleted skill's ACL caches with its
detail. The delete now removes every query under the skill's key, not
only the exact detail; a new case in `api/queries/skills.test.ts` (now
`organizationSkills.test.ts`) covers it.
19. `f82573cd1c2` fix: carry only a skill-readable organization on the
Skills link, with a table-driven `AISettingsSidebar.test.tsx` case.
20. `0e55137de7b` fix: share the skill-readable organization filter
(`SkillsPage/readableSkillsOrganizations.ts`, used by the sidebar and
`SkillsPage`) and cover an organization that is listed but unreadable.
21. `97d42fd3cd8` refactor: split skill queries by owner.
`queries/skills.ts` goes back to main's `userSkills.ts` (main's names
and keys) plus a new `organizationSkills.ts` for the organization and
ACL queries; the table takes a `SkillsScope` instead of an owner union.
22. `4ddfb80ac54` refactor: invalidate skill lists after writes, instead
of patching the cached list; update still writes the detail from the
PATCH response.
23. `14d5b755087` refactor: pass skill organizations to the sidebar as
`Organization` objects instead of names.
24. `463df6a5e69` feat: pick one organization on the member skills page.
25. `9c802557b9c` refactor: tighten the skill query comments and tests.
26. `9002dc60382` test: cover the failed sharing save in Vitest
(`OrganizationSkillSharingDialog.test.tsx`).
27. `a9df756b976` and `5ebf7b8984a` merge the updated sharing layer,
which brings in main. `a9df756b976` moves the `toast` imports in
`OrganizationSkillSharingDialog.tsx` and `SkillEnabledSwitch.tsx` to
`#/components/Toaster/toast`, as main's Biome import rule now requires,
and resolves conflicts with main in the model and MCP server sharing
dialogs.
28. `ac3e62c83b7` test: assert the member page's organization switch
through requests. After a switch, the test checks that the list and the
viewed skill are requested from the selected organization (a Codex
review finding).
29. `63bb42754e3` fix: show read-only skill status instead of a disabled
switch (from jaaydenh's review). `SkillEnabledSwitch` loses its
`disabled` prop; a new `ReadOnly` story in `SkillsTableView.stories.tsx`
shows the icons, and the read-only case in `SkillsTable.test.tsx`
asserted the status icon and no switch (removed again in item 30).
30. `82b2487ad17` test: address Codex review on skills tests. The
`SkillsPage.test.tsx` update-only case now edits the description, saves,
and asserts the `updateOrganizationSkill` call. The read-only status
case in `SkillsTable.test.tsx` is removed because the `ReadOnly` story
covers that visual state, and `renderTable` loses its `access` argument.
`AISettingsSidebar.test.tsx`'s hoisted permissions mock drops its cast
for an explicit return type.
31. `658be8b8df5` merges the updated sharing layer (main at
`e7b76872098`); no changes of its own.

## Size

Production +1465/-617, tests and stories +1104/-141 (tests +690 net,
stories +273 net), generated +4/-0; total +2573/-758. The site changes
in this PR and #30613 net +2263 lines, against +3058 for the same scope
in the old stack. This is above the 500-line reviewer-friendly limit on
purpose: Mike asked for 4 to 5 PRs instead of the 21 small ones this
stack replaces.

## Replaces

#30590 (site rename), #30595 (site API), #30596 (skills table), #30597
(Enabled toggle), #30598 (organization page), #30599 (ACL autocomplete),
#30600 (permissions), #30603 (member page).

## Validation

- `pnpm run lint` clean (tsc, oxlint, knip, React Compiler) and Biome
clean; `pnpm run lint` reran clean on `13f03d095ff`, which contains this
PR up to `0e55137de7b`.
- Vitest: 9 files, 49 tests. Storybook Vitest: 7 files, 101 tests. These
full runs were on `a52f9ba659c`; on `0e55137de7b`, the three test files
the later fixes change (`api/queries/skills.test.ts`,
`AISettingsSidebar.test.tsx`, `SkillEditor.test.tsx`) and the two
`SkillsPage` tests that cover the changed `SkillsPage.tsx` pass, 20
tests.
- On `a6f8f63ad13`, which contains this PR up to `5ebf7b8984a` and both
main merges: 38 Vitest files (the skill queries, the sidebar, the skill
pages and components, the slash menu, and `pages/AgentsPage/utils`)
pass, 542 tests with 2 skipped.
- On `63bb42754e3`: `pnpm check` and the full `pnpm run lint` pass;
Vitest (`SkillsTable`, `SkillsPage`, `AgentSettingsSkillsPage`: 6 tests)
and Storybook Vitest (the `SkillsTableView`, `AgentSettingsSkillsPage`
and `SkillsPage` stories: 34 tests) pass; `coder-frontend-review`
passed.
- On `82b2487ad17`: `pnpm check`, the full `pnpm run lint`, and Vitest
on the three changed test files (7 tests) pass.
- On `658be8b8df5`: `go build`, `pnpm check` and the full `pnpm lint`
pass; 6 Vitest files with 39 tests pass.
- `frontend-review` (FE1 to FE10) and `coder-frontend-review` (8
reviewer roles); findings fixed or declined as noted above.
- Remote UAT round 7 through Coder Agents passed on the whole stack,
last on `13f03d095ff`, which contains this PR up to `0e55137de7b`; see
#30613's Validation for scope and findings.
- Remote UAT round 8 passed on `b03c14c86de` (one item graded on intent;
see #30613), which contains this PR up to `9c802557b9c`: the member page
organization picker, list refresh after writes on the personal and admin
tables, no stale ACL after deleting and recreating a skill, and the
sidebar Skills `?org=`. See #30613 for details. `9002dc60382`,
`ac3e62c83b7`, `63bb42754e3`, `82b2487ad17` and the main merges have
tests but no UAT run.

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-13 16:57:41 +00:00
Michael Suchacz 2c432a12a3 feat: add organization skill sharing ACL routes (#30611)
## Stack

Part of
[CODAGT-1341](https://linear.app/codercom/issue/CODAGT-1341/support-organization-level-skills-in-coder-agents).
This stack adds organization skills to Coder Agents: admins publish a
skill once per organization, share it with groups or members, and chats
in that organization load it next to personal and workspace skills. It
replaces the 21-PR draft stack #30584 to #30604 with five PRs, bottom to
top:

1. #30609 data model: one migration, queries, RBAC and dbauthz
2. #30610 organization skill API routes and the scope-neutral SDK
3. **#30611** sharing ACL routes (this PR)
4. #30612 settings UI: shared skills table, admin page, member section
5. #30613 chats: chatd loading, `/` menu, docs

## What changes

This PR adds the routes that let admins choose who can read an
organization skill. It reuses the MCP server ACL machinery rather than
adding a second implementation.

Routes under
`/api/experimental/organizations/{organization}/skills/{skillName}`, all
behind `httpmw.ExtractOrganizationSkillParam` with `share`, so a caller
without share gets 404:

- `GET /acl` returns the resolved user and group grants.
- `GET /acl/available` returns the organization's users and groups that
can be granted, with the same `q`, `after_id`, `limit` and `offset`
paging as the MCP endpoint.
- `PATCH /acl` takes a sparse `UpdateOrganizationSkillACLRequest`: only
the listed principals change, `read` grants and `""` removes. It returns
204 and writes a `write` audit entry with the old and new ACL.

**Concurrency (please review closely):** the PATCH transaction first
locks the granted users' `organization_members` rows `FOR KEY SHARE`
(new query `LockOrganizationMembersByUserIDsForShare`, run as system),
then locks the skill row `FOR UPDATE`, then validates the update on the
transaction handle. Member removal takes the same locks in the same
order (its membership row, then the skill rows through the #30609
trigger), so the two can't deadlock, and a removal can't miss a grant
written by a concurrent PATCH. A skill deleted between the middleware
read and the row lock stays a 404.

Grants are users and groups, the same as MCP server configs (Mike's
decision, for parity). Only the `read` role exists; any other role is a
validation error, as are users or groups outside the organization.

Supporting changes:

- `coderd/mcp_acl.go`: the MCP ACL handlers' resolution, candidate
listing and validation move into generic helpers
(`writeOrganizationACLAvailable`, `validateOrganizationACLUpdate`,
`resolveACLUsers`, `resolveACLGroups`) shared by both resources;
`acl.Validate` accepts a named `acl.Role` constraint that now includes
`codersdk.OrganizationSkillRole`.
- `codersdk/skills.go`: `OrganizationSkillRole`, `OrganizationSkillACL`
with its user and group entries, `UpdateOrganizationSkillACLRequest`,
and client methods.
- New sqlc query `UpdateOrganizationSkillACLByID` with its dbauthz
wrapper (requires `share`).

## Commits

The first commit reproduces the reviewed code from the old stack; each
later commit is a labeled trim or fix.

- `b1c85560b69` feat: add organization skill sharing ACL routes.
Reproduction: every file this commit changes equals the reviewed old
`acl-routes` head `80c1c3594e6`; the rest of the tree differs only by
#30609's migration merge and main.
- `b5d2ee265c8` test(coderd): merge duplicate organization skill ACL
route tests (+49/-111). The removed cases repeated
`TestMCPServerConfigACL`, `TestMCPServerConfigACLAvailable` or
`TestRolePermissions`; with the MCP tests included, the covered block
set of `organizationskills_acl.go`, `mcp_acl.go` and
`rbac/acl/updatevalidator.go` is identical before and after. Kept:
`TestOrganizationSkillACL` (`OrgAdminManagesACL`, `ReadersGetNotFound`,
`SparseUpdateChangesReaders` with the audit diff,
`RejectsUnsupportedRole`, `ScopedAPIKeys`),
`TestOrganizationSkillACLConcurrentDelete`, and the lock order tests
`TestOrganizationSkillACLMemberRemoval` (`RemovalBeforeGrant`,
`RemovalAfterGrant`). `OrgAdminManagesACL` now also checks that users
from another organization are not candidates (mutation checked). Sharing
the param extractor with MCP was skipped: it would save about 10 lines
of security-sensitive code.
- `e09c4dba66b` Merge the updated org-api layer into sharing. Brings in
#30610's `ad0a7a37504`; the merge has no changes of its own.
- `1d3817dc639` and `c8c88f84d18` merge the updated #30610 layer,
including main; no changes of their own.
- `c4d22c47990` merges the updated #30610 layer (main at `e7b76872098`);
no changes of its own.

## Size

Production +431/-102, tests +377/-0, generated +918/-0; total
+1726/-102. This is above the 500-line reviewer-friendly limit on
purpose: Mike asked for 4 to 5 PRs instead of the 21 small ones this
stack replaces.

## Replaces

#30591 (ACL queries), #30592 (ACL routes).

## Validation

- `go build` and `go vet` passed; `make gen` exited 0 with no diff.
- `go test` passed for `coderd` (skill, MCP and ACL tests),
`coderd/database`, `dbauthz`, `migrations`, `gentest`,
`coderd/rbac/...`, `coderd/httpmw`, `coderd/x/skills/...`,
`coderd/x/chatd/...`, and the audit and skill tests in
`enterprise/audit` and `enterprise/coderd`.
- The covered block comparison above, before and after the test merge.
- On `c4d22c47990`: the same checks as #30610's head, plus `go test
./coderd/ -run 'Skill|MCP|ACL'`, pass.
- Remote UAT round 7 through Coder Agents passed on the whole stack; see
#30613's Validation for scope and findings.

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-13 16:43:15 +00:00
Michael Suchacz db7f5a7703 feat: add organization skill API routes (#30610)
## Stack

Part of
[CODAGT-1341](https://linear.app/codercom/issue/CODAGT-1341/support-organization-level-skills-in-coder-agents).
This stack adds organization skills to Coder Agents: admins publish a
skill once per organization, share it with groups or members, and chats
in that organization load it next to personal and workspace skills. It
replaces the 21-PR draft stack #30584 to #30604 with five PRs, bottom to
top:

1. #30609 data model: one migration, queries, RBAC and dbauthz
2. **#30610** organization skill API routes and the scope-neutral SDK
(this PR)
3. #30611 sharing ACL routes
4. #30612 settings UI: shared skills table, admin page, member section
5. #30613 chats: chatd loading, `/` menu, docs

## What changes

This PR adds the organization skill CRUD routes and makes the skill SDK
types owner-neutral so personal and organization skills share them.

**API contract change (experimental API, personal and organization
skills):** skill list and get responses gain `enabled`, and `PATCH
.../skills/{skillName}` now takes `{content?, enabled?}`, so `content`
is optional and a toggle doesn't resend it. A PATCH with neither field
returns 400; `content`, when present, is validated as before. In the
SDK, `UserSkill`, `UserSkillMetadata`, `CreateUserSkillRequest` and
`UpdateUserSkillRequest` become `Skill`, `SkillMetadata`,
`CreateSkillRequest` and `UpdateSkillRequest` (`codersdk/userskills.go`
moves to `codersdk/skills.go`), and `UpdateSkillRequest.Content` is now
`*string`. The personal client method names are unchanged. The site
changes are the handwritten edits that rename forces (`api.ts`,
`queries/userSkills.ts`, `testHelpers/skills.ts` and three call sites),
with no UI change.

Organization routes under
`/api/experimental/organizations/{organization}/skills`, not gated by an
experiment or license, like the personal ones:

- `GET` lists with the caller's own permissions, so admins, the site
owner and org auditors see every skill and members see only skills
shared with them. `GET /{skillName}` goes through the new
`httpmw.ExtractOrganizationSkillParam` and admits only read, so a
missing skill and a denied one both return 404.
- `POST` checks `create` on the organization before reading the body,
then seeds `group_acl` with the Everyone group (whose ID is the
organization ID) so a new skill reaches every member until an admin
narrows it. The 100-skill cap and a duplicate name return 409.
- `PATCH` admits only callers who can read the skill, because its
response includes the content; `DELETE` admits callers who can read or
delete it. A reader who can't change the skill gets a write-specific 403
from the database wrapper, and everyone else gets 404. PATCH locks the
row as system, updates as the caller in the same transaction, and audits
the locked row only after the transaction commits.
- Audit: `database.AuditableOrganizationSkill` records resource type
`organization_skill` with the organization ID; personal skills keep
`user_skill`.

Personal and organization handlers share one create and update path in
`coderd/userskills.go` (`createSkill`, `updateSkill`, and a
`skillWriteErrors` table per owner and action).

## Commits

The first commit reproduces the reviewed code from the old stack; each
later commit is a labeled trim or fix.

- `49cb2c84276` feat: add organization skill API routes. Reproduction:
every file this commit changes, including the SDK layer's site edits,
equals the reviewed old `write-routes` head `aa597100f28`; the rest of
the tree differs only by #30609's migration merge and main.
- `dc893bc60d6` refactor(coderd): share the skill create and update path
across owners (production +164/-188). It keeps the organization create
check before the body read, the lock as system then update as caller
order, and audit assignment after the transaction. No swagger,
`codersdk` or `typesGenerated.ts` change; `make gen` is clean.
- `f94925425ef` test(coderd): merge duplicate personal and organization
skill tests. `TestSkillsCRUD`, `TestSkillValidationAndConflicts` and
`TestSkillAudit` are table-driven over Personal, OrgAdmin and SiteOwner,
alongside `TestOrganizationSkillAuditsDeniedWrites` and one
`ScopedAPIKeys` subtest; `TestPatchUserSkill` is deleted because none of
its blocks were unique. Coverage of `userskills.go`,
`organizationskills.go` and `httpmw/organizationskillparam.go` is 101 of
117 blocks before and after, the same block set.
- `ad0a7a37504` fix(coderd): require read access to patch an
organization skill. The organization PATCH route now admits only `read`,
because its response carries the skill's content: a key scoped to
`organization_skill:update` without read gets 404, like GET, instead of
200 with the content. Readers without update still get 403, and DELETE
is unchanged. `TestOrganizationSkillAccess/ScopedAPIKeys` asserts the
404, that the response has no content, and that the skill is unchanged;
against the old route it fails with 200.
- `69d00ded0b6` and `f67b79343ec` merge the updated #30609 layer,
including main; `69d00ded0b6` resolves a conflict in the personal skills
page with main. No other changes of their own.
- `18373ffd59e` merges the updated #30609 layer (main at `e7b76872098`)
and reapplies the `AuditableOrganizationSkill` entry in
`enterprise/audit/table.go` alongside main's new
`AuditableChatProjectInstructions` entries.

## Size

Production +838/-308, tests +733/-322, generated +801/-274; total
+2372/-904. This is above the 500-line reviewer-friendly limit on
purpose: Mike asked for 4 to 5 PRs instead of the 21 small ones this
stack replaces.

## Replaces

#30587 (scope-neutral SDK and personal enabled state), #30588 (read
routes), #30589 (write routes and audit).

## Validation

- The coverage comparison above, before and after the test merge.
- `make gen` leaves no diff after the shared create and update refactor.
- On `18373ffd59e`: `make gen` is clean; build, vet, the `coderd` and
`enterprise/coderd` skill tests, `rbac`, `httpmw`, `dbauthz` and
`enterprise/audit` pass; `pnpm check`, `pnpm lint` and `make pre-commit`
pass.
- Remote UAT round 7 through Coder Agents passed on the whole stack; see
#30613's Validation for scope and findings.

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-13 16:24:45 +00:00
Michael Suchacz b67587c875 feat: add the organization skills data model (#30609)
## Stack

Part of
[CODAGT-1341](https://linear.app/codercom/issue/CODAGT-1341/support-organization-level-skills-in-coder-agents).
This stack adds organization skills to Coder Agents: admins publish a
skill once per organization, share it with groups or members, and chats
in that organization load it next to personal and workspace skills. It
replaces the 21-PR draft stack #30584 to #30604 with five PRs, bottom to
top:

1. **#30609** data model: one migration, queries, RBAC and dbauthz (this
PR)
2. #30610 organization skill API routes and the scope-neutral SDK
3. #30611 sharing ACL routes
4. #30612 settings UI: shared skills table, admin page, member section
5. #30613 chats: chatd loading, `/` menu, docs

## What changes

This PR adds the data model and authorization for organization skills.
Personal skills keep working unchanged: `git diff origin/main --
codersdk site` shows only generated RBAC and API key scope additions
(`apikey_scopes_gen.go`, `rbacresources_gen.go`,
`rbacresourcesGenerated.ts`, the RBAC and scope unions in
`typesGenerated.ts`) and `codersdk.ResourceTypeOrganizationSkill` with
its friendly string. No personal route, SDK type or site component
changes, and `coderd/userskills_test.go` changes only by mechanical
constant renames.

**One migration, `000621_skills`** (please review it closely):

- renames `user_skills` to `skills` with its constraints, index and
triggers, and recreates the cap function and
`delete_deleted_user_resources`, whose PL/pgSQL bodies name the table;
- adds `organization_id` and `project_id` (`ON DELETE CASCADE`),
`enabled` (default true), and `group_acl` / `user_acl` (`jsonb`, default
`{}`), with CHECKs for exactly one owner, ACLs that are JSON objects,
and ACLs that stay empty unless an organization owns the row, plus
partial unique name indexes per owner kind;
- replaces the cap trigger with `enforce_skills_per_owner_limit()`: 100
rows per owner, disabled rows included, locking the owner row `FOR NO
KEY UPDATE` so unrelated foreign key checks are not blocked;
- adds a `BEFORE DELETE` trigger on `organization_members` that removes
a departing member from `user_acl` on that organization's skills, so
rejoining doesn't restore a direct grant;
- adds the `organization_skill` `resource_type` and `api_key_scope` enum
values last; nothing in the migration uses them.

The down migration undoes the three old migrations in reverse order: it
drops organization and project rows and restores `user_skills`, its
names, the cap function and `user_id NOT NULL`.

Authorization: a new RBAC resource `organization_skill` (create, read,
update, delete, share). Owner and org admin get it; site and org
auditors get read; members read only through the skill's ACL, which
applies only to members of its organization. `Skill.RBACObject()`
returns the organization object with both ACLs; project rows stay denied
to everyone. New sqlc queries cover organization insert, get, list
(SQL-filtered through `GetAuthorizedOrganizationSkillMetadata` and a
`regosql` converter), update and delete, with dbauthz wrappers and the
audit table entries. No routes yet.

## Commits

The first commit reproduces the reviewed code from the old stack; each
later commit is a labeled trim or fix.

- `baa3150e3b7` feat: add the organization skills data model.
Reproduction: every non-site file this commit changes equals the
reviewed old `authz` head `1e16b3004c4`, except that the old stack's
migrations 000615, 000616 and 000617 are merged into one migration (then
`000615_skills`, now `000621_skills`) with one fixture
(`testdata/fixtures/000621_skills.up.sql`) and one migration test (now
`testMigration000621Skills`). At that commit, `dump.sql` was
byte-identical to the old head's.
- `1b84e83463a` test(coderd/database): keep only the SQL paths of the
org skill matrix. `TestOrganizationSkillAuthorization` becomes
`TestOrganizationSkillQueries` instead of being deleted, because
coverage showed it is this PR's only run of the
`GetAuthorizedOrganizationSkillMetadata` row scan (80% to 65% without
it) and of the organization update and delete queries (100% to 0%).
Swapping `group_acl` and `user_acl` in the converter makes
`GrantedMember` fail. The removed cases are decided by
`TestRolePermissions`, `TestSkillRBACObjectOrganizationACL` and the
dbauthz method suite.
- `e2931b412e9` test(coderd/database): fold the skill cap cases into
existing coverage. The user case is covered by `TestUserSkillLimit` and
`TestUserSkillLimitConcurrentCreates`; the organization case folds into
`ConcurrentOrganizationInsertsHonorCap`, which now also checks that
disabled rows count and another organization is unaffected (mutation
checked); the project case stays.
- `be62aea9021` test(coderd/rbac): drop
`TestOrganizationSkillReadScope`, covered by `TestExternalScopeNames`
and the generated scope file.
- `0c275ba37de` merges main at `454a2fcc697`, which took 000615 to
000618: it renumbers the migration to 000619 and resolves `dump.sql`,
`check_constraint.go` and the generated API docs. `8f2b4b87ee2` merges
main at `ad85646164e`, which took 000619: it renumbers the migration to
000620.
- `5248d56b64f` merges main at `e7b76872098`, which took 000620
(`000620_audit_chat_project_instructions_resource_type`): it renumbers
the migration, fixture and migration test to `000621_skills` (step
`Migration000621Skills`, version 621), and resolves
`enterprise/audit/table.go` (keeping main's
`AuditableChatProjectInstructions` entries and the `Skill` entry),
`dump.sql` and `models.go` (the `resource_type` enum gets
`chat_project_instructions`, then `organization_skill`) and the
regenerated `schemas.md`. The up and down SQL are unchanged by every
renumbering.

## Size

Production +549/-67, tests +728/-33, generated +881/-314; total
+2158/-414. This is above the 500-line reviewer-friendly limit on
purpose: Mike asked for 4 to 5 PRs instead of the 21 small ones this
stack replaces.

## Replaces

#30584 (rename `user_skills` to `skills`), #30585 (skill owners), #30586
(organization skill authorization).

## Validation

- Migration test: up keeps the existing personal row and its ID; down
restores the `user_skills` names, cap function and `NOT NULL`, and drops
organization rows; soft-deleting a user removes their skills after up,
after down, and after re-up.
- `go test` passed on `be62aea9021`, before the main merges, for
`coderd` (skill tests), `coderd/database`, `dbauthz`, `migrations`,
`gentest`, `coderd/rbac/...`, `coderd/x/chatd`, `coderd/x/skills`,
`enterprise/audit` and `enterprise/coderd`; `make lint/check-scopes`
passed; the four personal skills Vitest files pass unchanged.
- On `5248d56b64f`: build, vet, `TestMigrate`, `TestMigrationChain`, the
`migrations`, `database`, `dbauthz`, `gentest`, `rbac` and
`enterprise/audit` packages pass; `make gen` run twice is clean, and
`make pre-commit` passes.
- Remote UAT round 7 through Coder Agents passed on the whole stack; see
#30613's Validation for scope and findings.

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-13 15:51:31 +00:00
Mathias Fredriksson e7b7687209 fix(site): keep paragraph breaks in streamed chat text (#30551)
The chat UI dropped every whitespace-only text or reasoning delta.
Providers can stream a paragraph break as its own "\n\n" delta, so the
live preview showed "writing.Like this", and headings and list items
that need a line start rendered inline, until the saved message
replaced the preview. Interrupted turns, saved with one part per
delta, kept the joined text permanently.

Whitespace-only text now extends the preceding block of the same type
and is dropped only where it would start a block. A leading
whitespace delta still leaves the stream state null, so it does not
end the "starting" phase early, which is what the old check was for.
2026-10-13 15:47:21 +03:00
Danielle Maywood ff5edf2c3a refactor(site/src/pages/AgentsPage/components/ChatElements/tools): add ToolCall.Scrollable (#30648) 2026-10-13 13:24:12 +01:00
97c5260adb feat: add chat project instructions API (#30368)
Adds the experimental API for chat project instructions: `GET`, `PUT`,
and `DELETE
/api/experimental/organizations/{organization}/chats/projects/{project}/instructions`,
behind the `chat-projects` experiment.

- `GET` returns 200 with empty instructions and null
`updated_by`/`updated_at` when nothing is set, so clients don't need a
404 path.
- `PUT` requires update access only (the read-back runs under a system
context, so a custom role with update but not read can save). It
sanitizes the text with `SanitizePromptText`. It rejects blank text with
400, pointing the caller to `DELETE`, and rejects text over the chat
prompt byte limit. It records the caller as `updated_by`. The upsert and
the read-back that returns the editor's profile run in one transaction.
- `DELETE` returns 204, including when nothing is set.
- Users without access to the project get 404, like the other project
routes.
- `PUT` and `DELETE` are audited as a new `chat_project_instructions`
resource (`write` and `delete`), with the project as the resource ID and
its name as the target. The diff tracks only the instruction text.
Saving unchanged text and deleting unset instructions are not audited. A
per-project advisory lock serializes reading the previous value with the
write, so concurrent edits produce accurate diffs. Migration `000615`
adds the enum value.

The codersdk client gains `ChatProjectInstructions`,
`UpdateChatProjectInstructions`, and `DeleteChatProjectInstructions`.

**What to scrutinize:** writes are last-writer-wins with no version
precondition, matching the deployment, user, and organization prompt
settings.

**Generated:** `coderd/apidoc/docs.go`, `coderd/apidoc/swagger.json`,
and `docs/reference/api/schemas.md` come from the Swagger annotations;
`site/src/api/typesGenerated.ts` comes from `codersdk`;
`docs/admin/security/audit-logs.md`, `coderd/database/dump.sql`, and
`coderd/database/models.go` come from the audit table and migration.
Together they are about 400 of this PR's lines.

**Validation:** `make gen` leaves no diff. `go test ./coderd -run
TestChatProject` and `go test ./enterprise/coderd -run
TestChatProjectInstructions` pass. `TestChatProjectInstructionsAudit`
checks the audit entries for create, update, and delete, and that no-op
saves and deletes are not audited. It covers unset GET, create, update,
blank and oversized PUT, delete, access denial, and cascade on project
delete.

---

Stack (merge top to bottom). #30377 through #29583 are f0ssel's split of
the original #29583, and this stack sits on #29583:
- #30377 data layer and project dialog (f0ssel)
- #30378 new-chat form locked to a project (f0ssel)
- #30379 project page route (f0ssel)
- #29583 sidebar folders (f0ssel), the base of this stack
- #30291 composer frame
- #30292 project chat list caches
- #30293 project page header
- #30294 project chats list
- #30367 instructions storage and prompt injection
- #30368 instructions API 👈
- #30369 instructions editor dialog
- #30370 instructions Details panel

---------

Co-authored-by: Garrett Delfosse <delfossegarrett@gmail.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 19:15:16 +07:00
Danielle Maywood 454a2fcc69 fix(site/src): show a loader instead of the error page during stale chunk reload (#30658) 2026-10-13 08:27:20 +00:00
christinandAndrew Aquino 4e4603eae9 fix(site/src/pages/UserSettingsPage/AppearancePage): raise theme swatch ring contrast to 3:1 (#30539)
The ring around each theme swatch in the Appearance settings (Theme mode
set to "Sync with system") used the decorative border color: `#27272A`
on the `#09090B` page, 1.33:1. The Dark default swatch is filled with
the page color, so that ring is its only visible boundary, which fails
WCAG 1.4.11 Non-text Contrast.

The ring now uses the existing `surface-invert-secondary` token, which
the Slider handle already uses for its border: `#A1A1AA` on the dark
page (7.8:1) and `#3F3F46` on white (10.5:1). The blue selected and
focus outline is unchanged.

Fixes DEQ-597

## Before and after

Appearance > Theme mode "Sync with system", resting state. Dark default
is the first swatch.

| | Before | After |
|---|---|---|
| Dark page | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/assets/a11y-theme-swatch-contrast/dark-before.png"
/> | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/3e8883931834186feab055d104b7c62fb352240d/dark-after.png"
/> |
| Light page | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/assets/a11y-theme-swatch-contrast/light-before.png"
/> | <img width="360"
src="https://raw.githubusercontent.com/coder/coder/3e8883931834186feab055d104b7c62fb352240d/light-after.png"
/> |

<details>
<summary>Decision log</summary>

- **Resting border, not a focus state.** 1.4.11 requires the control's
boundary to be visible without interaction. The swatches already show a
blue `content-link` outline when focused or selected, and the audit
flagged the unselected, unfocused swatches.
- **Existing token instead of a new one.** `surface-invert-secondary`
already exists on `main` and is used as a control border by the Slider
handle. It passes 3:1 in both modes. None of the existing `border-*`
tokens do on the dark page (`border-secondary` is 1.9:1).
- **Before images** were captured on the same dev server with the
previous border value (`hsl(var(--border))`) applied through an injected
style. Light-page images force the `light` class on `<html>`.

</details>

<details>
<summary>Verification</summary>

- Playwright on the dev server at `/settings/appearance`: the Dark
default swatch's computed border is `rgb(161, 161, 170)` on the dark
page (`rgb(9, 9, 11)`) and `rgb(63, 63, 70)` on the light page
(`rgb(255, 255, 255)`).
- `pnpm check` and `pnpm lint` pass.
- vitest unit (`src/pages/UserSettingsPage`, `src/theme`): 13 files, 399
tests pass.

</details>

---

_Generated by Coder Agents on behalf of @chrifro._

---------

Co-authored-by: Andrew Aquino <dawneraq@gmail.com>
2026-10-13 10:02:22 +02:00
christin d68b2c8b25 fix: keep error toasts visible until dismissed (#30420)
Error toasts now stay on screen until the user closes them, to meet WCAG
2.2.1 Timing Adjustable. Success, info, and warning toasts keep Sonner's
default timer.

Fixes
[DEQ-463](https://linear.app/codercom/issue/DEQ-463/timed-toast-without-user-control)
(Open Cursor Desktop failure) and
[DEQ-310](https://linear.app/codercom/issue/DEQ-310/timed-toast-without-user-control)
(adding a user who is already in a group).

Sonner has no default duration per toast type, so this adds
`site/src/components/Toaster/toast.ts`, a thin wrapper around Sonner's
`toast`:

- `toast.error` defaults to `duration: Infinity`. A caller can still set
its own `duration`.
- `toast.promise` failures get the same default. Sonner renders a
rejected promise without calling `toast.error`, and the group member
flow (DEQ-310) uses `toast.promise`.
- All other methods pass straight through.

All `toast` imports now go through the wrapper. A Biome
`noRestrictedImports` rule blocks importing `toast` directly from
`sonner`, following the existing `@pierre/diffs` rule. Tests that mocked
`sonner` now mock the wrapper. Most of the 130-file diff is the import
swap.

### Proof

Error toast on the create token page (duplicate name), with the pointer
off the toast so Sonner doesn't pause the timer. Shots are taken once
the enter animation has finished:

| | 1 second after the error | 11 seconds after the error |
|---|---|---|
| Before | ![before
1s](https://raw.githubusercontent.com/coder/coder/9f65d67d10d43265082ab4f60e48527239845061/pr/pr-before-1s.png)
| ![before
11s](https://raw.githubusercontent.com/coder/coder/9f65d67d10d43265082ab4f60e48527239845061/pr/pr-before-11s.png)
|
| After | ![after
1s](https://raw.githubusercontent.com/coder/coder/9f65d67d10d43265082ab4f60e48527239845061/pr/pr-after-1s.png)
| ![after
11s](https://raw.githubusercontent.com/coder/coder/9f65d67d10d43265082ab4f60e48527239845061/pr/pr-after-11s.png)
|

Also checked in the running app: the close button dismisses the error
toast, and a "Token created" success toast still disappears on its own.

`toast.test.tsx` covers the error and rejected `toast.promise` cases
staying visible, dismissing them with the close button, and success
toasts still timing out. With the persistent duration removed, the two
error cases fail.

<details>
<summary>Decision log</summary>

- A first version set `duration: Infinity` on the shared `<Toaster>` for
every toast type. Product feedback: only error toasts were reported, so
success and info toasts should keep their timer.
- The wrapper sets the duration when each toast is created. The
alternative was to watch toasts from `<Toaster>` with `useSonner` and
change them afterward. That would add subscription and timer-ordering
issues.
- I didn't change Sonner's exported `toast` object in place. The wrapper
plus a lint rule is explicit and enforced.
- The experimental chat board undo toast (`runPlan.ts`) uses `toast()`
with a timer that matches its undo window, so it isn't affected.
- Not reproduced end to end: the Groups flow needs a Premium license,
and the Cursor flow needs the app installed. Both use the
`toast.promise` and `toast.error` paths the tests cover.

</details>

---

_Generated by Coder Agents on behalf of @chrifro._
2026-10-13 09:21:53 +02:00
christin bc445defd6 fix(site/src/components): raise breadcrumb and topbar separator contrast (#30546)
Raises the contrast of the `/` separators in `BreadcrumbSeparator` and
`TopbarDivider` to meet WCAG 1.4.11 (non-text contrast, 3:1). Fixes
DEQ-163 and DEQ-418.

Both separators now use `content-secondary`, which measures 7.78:1
against `surface-primary` in dark mode and 7.69:1 in light mode. The
previous colors were `content-disabled` (1.90:1 dark, 2.55:1 light) and
`border-default` (1.34:1 dark, 1.27:1 light). `border-secondary` and
`content-disabled` resolve to the same values, so neither was a viable
fix.

Affects every breadcrumb in the settings layouts (deployment,
organization, user, template, workspace) and the topbar dividers on the
workspace page and the template version editor.

| | Before | After |
|---|---|---|
| Breadcrumb (`/deployment/overview`) | ![breadcrumb
before](https://raw.githubusercontent.com/coder/coder/pr-assets/fix-separator-contrast/breadcrumb-before.png)
| ![breadcrumb
after](https://raw.githubusercontent.com/coder/coder/pr-assets/fix-separator-contrast/breadcrumb-after.png)
|
| Topbar divider (template version editor) | ![topbar
before](https://raw.githubusercontent.com/coder/coder/pr-assets/fix-separator-contrast/topbar-before.png)
| ![topbar
after](https://raw.githubusercontent.com/coder/coder/pr-assets/fix-separator-contrast/topbar-after.png)
|

In the running app (dark theme), both separators now compute to
`rgb(161, 161, 170)` on `rgb(9, 9, 11)`. The topbar divider measured
`rgb(39, 39, 42)` before the change.

Design note: both tickets carry `ux-input-needed`. The separator now has
the same weight as secondary text. A dedicated, subtler separator token
at about 3:1 is an alternative if design prefers.

> [!NOTE]
> This PR was generated by Coder Agents on behalf of @chrifro.
2026-10-13 09:19:45 +02:00
Sas Swart 1320c010af feat(site): show provider type in model form and model list icon tooltip (#30353)
The model form and models list showed the provider type only through an
icon, which falls back to a generic icon for unmapped types like
`openai-compat`. Model prices are keyed by provider type, so admins
could not tell which type to use with `coder exp ai-model-prices update
--provider`.

- The model form shows `Provider type: <type>` under the provider
select, in both add and edit mode.
- The provider icon on each models list row shows a `Provider type:
<type>` tooltip on hover and has a matching accessible name.

Fixes AIGOV-686

<details>
<summary>Decision log</summary>

- An earlier revision rendered the raw type inline next to the label in
the provider select, the Add model menu, and the models list. When a
provider has no display name, the label falls back to its name, which
defaults to the type, so most rows read like "anthropic anthropic".
Review feedback suggested helper text under the form's select plus a
tooltip on the icon instead.
- The tooltip is only on the models list icon. Tooltips on icons inside
select options or menu items conflict with their hover and keyboard
handling, and the form's helper text already covers that flow.
- The icon trigger is a non-focusable `role="img"` span with an
`aria-label`, so it adds no tab stop inside the clickable row.
- Existing exact accessible-name assertions in the ModelForm and
ModelsPageView stories are restored; they guard the aria-hidden icon
fix.
- Coverage: the `ModelForm` `ShowProviderType` story (plus the existing
`Add` and `Edit` stories) captures the helper text for an
`openai-compat` provider. The `ModelRow` `UnmappedProviderType` story
hovers the generic icon so Pixel captures the tooltip.

</details>

> Generated by Coder Agents on behalf of @SasSwart.
2026-10-13 09:07:35 +02:00
Bobby Ho dae7967bd1 feat: persist script resource addresses and dependencies (#30625)
feat(coderd): persist workspace agent script dependencies

Persist Terraform resource addresses and script dependencies supplied by
the provisioner (#30451, #30491), and expose them through the SDK and
REST API.

- Add migration 000618 with `resource_address` and `dependencies`
  columns, empty defaults, and a JSON-array constraint. API responses
  always include the address and omit empty dependency lists.
- Preserve `success` and `completion` requirements, including scripts
  on devcontainer subagents. Legacy scripts and older provisioners
  remain compatible through empty defaults.
- Reject empty prerequisite addresses and unsupported requirements,
  rolling back the entire job. Invalid stored dependencies return HTTP
  500 rather than silently dropping ordering.
- Add persistence, validation, rollback, devcontainer, API, and migration
  coverage; regenerate API docs and TypeScript types.

Unordered templates retain their existing execution behavior.
Dependency lists remain empty until #30556. Agent manifest wiring and
the agent API version bump are deferred to a follow-up.

Refs PLAT-548.
2026-10-13 03:43:55 +00:00
Ehab Younes 2f2169ed19 fix: deduplicate template insights family metrics (#30497)
coderd_insights_applications_usage_seconds summed each family's per-app
minutes, so a user with VS Code and Cursor open in the same minute was
counted as two VS Code minutes. GetTemplateInsightsByTemplate now takes
the registry's app-to-family mapping as jsonb, groups the raw agent
stats by family, and counts each minute once per user and family.

- Keep the metric's four built-in series (Visual Studio Code, JetBrains,
  Web Terminal, SSH) and their labels; only their values change. The
  HELP text now says built-in apps are grouped by family.
- Add AppFamilyName.DisplayName() and remove the duplicate
  TemplateBuiltinAppDisplayName* constants from codersdk. The template
  insights handler returns the same display names.
- Pin how the migration 000596 rollback approximates family minutes
  from per-app rows.

Family values can drop wherever the old per-app sum double-counted
overlapping apps. No migration or storage changes.
2026-10-12 23:58:35 +03:00
Michael Suchacz 41f5caafec fix: fold web searches, answer reasoning, and OpenAI narration into working blocks (#30529) 2026-10-12 22:53:07 +02:00
TJ ca7d7afea9 fix(site/src/pages/AISettingsPage): align collapsible section chevron with title (#30654)
Moves the expand/collapse chevron in the AI settings collapsible
sections (Cost estimate, Provider configuration, and the MCP server form
equivalents) from `mt-0.5` to `mt-1` so it centers on the title's first
line. The `text-sm` title has a 24px line-height and the icon is 16px,
so it needs a 4px offset.

Fixes DES-22168

> Generated by Coder Agents on behalf of @tracyjohnsonux.
2026-10-12 15:53:18 -04:00
Jeremy Ruppel abc2faed60 fix(site): keep path separator in terminal websocket URL (#30573)
Enables oxlint's `no-unused-expressions` rule and fixes its violations,
two of which were bugs.

- `terminalWebsocketUrl` built `` `${url.pathname}/` `` without
assigning it, so a base URL with a path (for example a proxy at
`/prefix`) produced `/prefixapi/v2/...`. It now assigns `url.pathname`,
and a test covers it (the test fails without the fix).
- The deployment `experiments` e2e test called `expect(...).toBeVisible`
without `()` or `await`, so those two assertions never ran.

The remaining three hits were `a && f()` and ternaries used as
statements. These are rewritten as `if` statements with no behavior
change.

Depends on #30572

<details>
<summary>Context and findings</summary>

Part of evaluating oxlint's `correctness` category one rule at a time
(see #30572). This rule had 6 hits in 5 files.

The e2e fix makes two assertions active for the first time. Enabled
experiments render with `option-enabled` in `OptionValue`, so they are
expected to pass, but they were not run locally.

Validation: `pnpm run lint` (Biome, oxlint, types, circular deps, React
Compiler, knip) and the full unit suite.

</details>

_Generated by Coder Agents on behalf of @jeremyruppel._
2026-10-12 10:31:07 -04:00
Jeremy Ruppel 1c805b0931 fix(site): use page count in usePaginatedQuery overshoot fallback (#30572)
Enables oxlint's `erasing-op` rule and fixes the one violation it found,
an operator precedence bug in `usePaginatedQuery`.

When the requested page reports a count of 0, the hook refetches page 1
to compute where to clamp. `Math.ceil(firstPageResult?.count ?? 0 /
limit)` parsed as `count ?? (0 / limit)`, so it used the record count as
the page count and left the user on the overshot page. The fix adds
parentheses, and a regression test covers it (the test fails without the
fix).

<details>
<summary>Context and findings</summary>

Part of evaluating oxlint's `correctness` category. Turning on the whole
category reported 1152 diagnostics across 24 rules, so rules are being
enabled one at a time instead, which keeps the explicit allowlist model
in `.oxlintrc.jsonc`. `erasing-op` had a single hit, and it was a real
bug.

Validation: `pnpm run lint` (Biome, oxlint, types, circular deps, React
Compiler, knip) and the full unit suite.

</details>

_Generated by Coder Agents on behalf of @jeremyruppel._
2026-10-12 10:31:06 -04:00
Ehab Younes 5b060beeb1 feat: separate connection log methods from app names (#30466)
Connection logs now record how a connection was made
(`connection_method`) separately from which app made it (`app_name`).
Storage, the HTTP API, search filters, and the UI move to the new
fields. Legacy agent reports and the deprecated `type` field keep
working.

- Add `connection_method` (`ssh`, `reconnecting_pty`, `workspace_app`,
  `port_forwarding`, `tunnel`) and optional `app_name` and
  `app_display_name` to `GET /api/v2/connectionlog`. Deprecate `type`
  and make it a plain string in the API and the Go SDK.
- Add `method:` and repeatable `app:` search filters. `type:` keeps its
  old results but can't be combined with the new filters.
- Replace the Type menu with a Method menu, show the app next to the
  method in each row, and warn when a filter uses `type:`.
- Change the Active SSH connections preset to `method:ssh`, so it also
  includes SSH sessions from VS Code and JetBrains apps.
- Generate `codersdk.ConnectionLogMethod` from the database enum with
  `typegen dbenums`.
- Migration 000617 rewrites `connection_logs` under an ACCESS EXCLUSIVE
  lock, which takes about 50 seconds for 10 million rows. `vscode` and
  `jetbrains` rows become `ssh` rows with that app. Rolling back folds
  apps into the old types and loses per-app names.
- Document the new filters and how to replace each `type:` value.

Closes #27412
2026-10-12 15:56:40 +03:00
Danielle Maywood c735c1dd1d fix(site): keep agent chat scroll on tool toggles and log streams (#29354)
Help keep position better in Coder Agents chat transcript during active
scrolling

<details>
<summary>LLM slop description</summary>

In the agent chat, a tool call that expands or collapses can make the
transcript jump. The clicked row can scroll out of view, or the next
streamed output can pull the view to the bottom. The scroller cannot
tell a layout change from new output, so it reacts to a toggle as if the
reply grew.

`ToolCall` now tells the scroller before it toggles. The scroller then
holds the view in place until the reader scrolls, a scroll command runs,
or a new turn starts. The changes are `LOCAL CHANGE` edits to the
scroller source that #29473 vendored.

The `create_workspace` and `start_workspace` rows also made the
transcript jump while the workspace built. Their log boxes used
`scrollIntoView` to show new lines, and that also scrolls the
transcript. At the end of the build, the view could stop following the
reply. Now each box scrolls only itself, and only while the reader is at
its end, so a reader who scrolls up in the box keeps their place. Other
pages use the scroll in the shared `WorkspaceBuildLogs` component, so
that component does not change.

Four behavior changes are intentional. While a turn is anchored, a large
block of output no longer pulls the view to the bottom. The reader stays
on the turn and can use the jump-to-bottom button. Collapsing a tool
while the view follows the reply also stops following, so the rows near
the pointer stay put for the next click. A press on the scrollbar now
counts as a scroll, so it releases the held view like a wheel or key
scroll. Space on a button does not count as a scroll, because Space
activates the button.

Related upstream bug reports, all open with no upstream fix:
shadcn-ui/ui#11175, shadcn-ui/ui#11577, shadcn-ui/ui#11224

</details>

> [!NOTE]
> Generated with [Coder Agents](https://coder.com/docs/ai-coder/agents)


---


https://github.com/user-attachments/assets/deb75f78-5f4e-49b8-baa3-9e2196234756



https://github.com/user-attachments/assets/0f03c526-d60d-4533-8098-1f200e8f11c1



https://github.com/user-attachments/assets/4a014fd6-a3f0-42cd-b5fc-7a61b8f783f5



https://github.com/user-attachments/assets/25d7fb5f-51f1-412d-95ab-a4f3dbeeb167



https://github.com/user-attachments/assets/a1e08441-8d87-4779-96f0-cd676ae217a7



https://github.com/user-attachments/assets/81577372-3e07-4d5a-b732-8ea756d7b604



https://github.com/user-attachments/assets/7dd03bc3-f5f7-4ea9-b602-0369ad120575



https://github.com/user-attachments/assets/41393122-85a8-4e4d-b0f2-1e392ccc4cb9


https://github.com/user-attachments/assets/38caf176-2cb5-4920-9cbc-0262c2157a35



https://github.com/user-attachments/assets/5606106a-2466-4cdf-9fcf-4df38f45d20f



https://github.com/user-attachments/assets/03b1060d-82c5-4844-aaf2-12c29572ae8c



https://github.com/user-attachments/assets/4b966412-0278-4d41-ac2e-91de2a61d803



https://github.com/user-attachments/assets/64ff8cce-0491-46c6-a563-afcb726b2dd1



https://github.com/user-attachments/assets/5c005789-ce82-4d7c-a918-ce2d5ec819c4



https://github.com/user-attachments/assets/0b9bfcda-4971-47c6-802b-3ed4e78a2f75



https://github.com/user-attachments/assets/683ac4ef-cd02-470f-b301-8a80554b9122



https://github.com/user-attachments/assets/dc8da762-b4c3-42ef-93d9-d1a3b13eb7eb
2026-10-12 09:03:44 +01:00
Thomas Kosiewski f923f4a525 chore!: remove the chat automations experiment (#30412)
Removes the experimental Chat Automations feature (`chat-automations` experiment) from main. This reverts the 16-PR stack that added it and adds two forward migrations that undo its schema and credential changes.

> [!WARNING]
> **Upgrade consequences**
>
> - **Automation data is deleted.** Migration 000615 drops the `chat_automations` table and deletes every queued message an automation delivered. Chats, their messages and ordinary queued messages stay. Messages lose their automation provenance (`automation_id`, `input_id`).
> - **Automation-only credentials are revoked.** Migration 000616 deletes API keys, OAuth2 codes and OAuth2 tokens whose only scopes (or allow-list entries) were `chat_automation:*`. Mixed credentials keep their other scopes. Custom roles lose their `chat_automation` permissions. OAuth2 apps that allowed only `chat_automation` scopes get the whitespace allowlist that grants nothing, as in 000595.
> - **A coordinated restart is required.** Stop all old replicas before the new version runs the migrations. Old replicas read the dropped table and columns, so a rolling deploy is not supported.
> - **Remove `--chat-max-automations-per-owner` before upgrading.** The flag, the YAML key `chat.maxAutomationsPerOwner` and the env var `CODER_CHAT_MAX_AUTOMATIONS_PER_OWNER` are removed. A deployment that still sets the flag or the YAML key will not start (`unknown flag` / `unknown option`). The env var is ignored. This includes dogfood. No release or tag contains the flag.
> - **Down migrations restore the schema only.** 000616 down and 000615 down recreate the 000611 schema with empty tables. They do not restore deleted automations, webhook secrets, provenance, queued deliveries, revoked credentials, stripped permissions or the deleted experiment rule.

## What is removed

- The `chat-automations` experiment and its stored runtime rule (`experiment_rule:chat-automations`).
- The `--chat-max-automations-per-owner` deployment option.
- The `chat_automation` RBAC resource, its member and chatd grants, and the `chat_automation:*` API key scopes.
- The automation sqlc queries, dbauthz wrappers and audit table entries.
- The management routes under `/api/experimental/organizations/{organization}/chat-automations`, and the webhook route `POST /api/experimental/chat-automations/{automation}/events` with its rate limiter.
- The chatd management service, webhook publish, schedule loop, the `manage_automations` tool, and the chatstate admission seam and queue promotion guard that only automations used.
- The Agents Automations page, editor, nav entry, chat labels, the Manage automations toggle and the API client.
- The `archived:any` chat search value that the automations "View chats" dialog added.
- `docs/ai-coder/agents/automations.md`, its manifest entry, the glossary entry and the related rows.

Reverted PRs: #30085, #30096, #30112, #30117, #30127, #30134, #30160, #30166, #30167, #30171, #30187, #30207, #30214, #30217, #30219, #30220.

## What stays, and why

- Migrations 000609, 000610 and 000611, their fixture, and the #30297 lock fix with `migration000609_test.go` stay unchanged. Deployments such as dogfood already ran them.
- New migration **000615** reverses the schema changes of 000609 and 000611 against the current schema. It uses the same short-wait lock-and-retry block as 000609, and it rebuilds `chats_expanded` from main's current definition minus the two automation columns. New migration **000616** follows the 000595 Tasks removal for credentials and recreates `api_key_scope` without the five `chat_automation:*` values. It keeps the `chat_project_memory:*` values from 000612.
- The `chat_automation` value stays in the `resource_type` enum, and `codersdk.ResourceTypeChatAutomation` stays. Old audit log rows stay readable, as with `task` after the Tasks removal.
- The 000615 delete fires the existing queue-version trigger, so `queue_version` moves to `snapshot_version` for each affected chat. `snapshot_version` is not bumped: the coordinated restart restarts every stream loop from an empty state, and the site does not read the version fields.
- `coderd/x/chatd/ARCHITECTURE.md` drops the sections the stack added and gets back its pre-stack wording, merged with the later main edits to the same file.

## Notes for reviewers

- #30379 (chat project page) merged while this PR was in progress. It passed `manageAutomationsEnabled` in `AgentCreatePage.test.tsx` and listed an `automations` route in `AgentsPageLayout.test.tsx`. Later main commits also added automation props in `ConversationTimeline.test.tsx` and `ConversationTimelineWorkingBlocks.stories.tsx`. This PR removes all of them.
- Remaining `automation` matches under `coderd`, `codersdk`, `enterprise`, `site/src` and `docs` are intended:
  - migrations 000609 to 000611, their fixture and `migration000609_test.go`;
  - the new 000615 and 000616 migrations and `migration000615_test.go`;
  - the `chat_automation` resource type in `dump.sql`, `models.go`, `codersdk/audit.go`, the generated API docs, `swagger.json` and `typesGenerated.ts`;
  - unrelated uses of the word "automation" (docs, `chatlabels_test.go`, `ratelimit.go`, `autostop.go`, `subagent_catalog.go`, `WorkspaceSharingSection.tsx`, `AgentChatPage.stories.tsx`).

Remote UAT on an isolated dev deployment passed. It upgraded a populated pre-removal database, then checked chats, queued messages, tokens, the 404 routes, the removed page and nav entry, and the experiments list.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

![Agents sidebar after the upgrade, with no Automations entry](https://github.com/user-attachments/assets/bd446035-7fc8-4ca4-a735-3a4f36380738)
2026-10-11 19:57:49 +02:00
Michael Suchacz 2d96e88ebe fix(site/src/api): abort chat history requests when the query is cancelled (#30542)
Chat history requests now honor React Query's abort signal.
`getChatMessages` takes an optional `signal`, and
`chatMessagesForInfiniteScroll` passes the query's signal to the first
page request and to every turn fill request.

## Stack context

Stacked on #30524, which loads whole chat turns per history page by
following the first page request with fill requests back to the turn's
prompt.

## Why

A review question on #30524 asked whether the fill loop bypasses React
Query. It does not (the merged page is cached under `chatMessagesKey`
and a refetch reruns the queryFn per page), but no history request
received the abort signal, so a cancelled query kept its HTTP requests
running. With this change, leaving a chat or `cancelChatMessages` in the
edit mutation aborts the in-flight page and fill requests. An aborted
fill is caught by the existing catch/break, and React Query discards the
result.

Behavior change: because the queryFn now reads `signal`, unmounting the
last observer cancels and reverts an in-flight history fetch instead of
letting it finish in the background.

## Testing

- `pnpm exec vitest run src/api/queries/chats.test.ts`: 247 passed.
- New test `aborts the page and fill requests when the query is
cancelled`; the existing first-request and per-page-cap assertions now
expect the signal.
- Red-green: removing the signal from the first request, from the fill
request, or from the queryFn each fails the new test.
- `pnpm lint:types`, `biome check --error-on-warnings`, `oxlint
--deny-warnings` on touched files: clean. Pre-commit hook passed.

## Frontend review

- `frontend-review` FE1 to FE10: all pass.
- `coder-frontend-review` round 1: SD1 (single-use `history` binding in
the new test) fixed. TN3 (wrap the factory in `infiniteQueryOptions`)
declined: the hand-typed queryFn context is pre-existing on main, and
converting it would change the factory's type and every direct `queryFn`
call in tests, which is outside this change. All other roles: no
findings.

> Xum opened this PR on behalf of @ibetitsmike.
2026-10-11 12:13:46 +02:00
Michael Suchacz c28ae0dc9b feat: trigger compaction independently for the chat and compaction models (#28568)
> [!NOTE]
> Xum (an AI agent) authored this pull request on Mike's behalf.

## Summary

Automatic compaction now fires at whichever of two independent triggers
is reached first, and the UI shows when the organization override's
trigger applies.

- **Chat trigger**: the chat owner's personal threshold (or the chat
model's) against the chat model's window. A threshold of 100 turns off
only this trigger.
- **Organization trigger**: a fixed 80% of the organization compaction
override model's window. The override model's own compression threshold
is ignored. The other 20% is a target margin for the summary prompt and
the summary, not a guarantee, because the trigger reads the previous
step's prompt size.

On main, the chat threshold was applied to the smaller of the two
windows, and nothing in the UI showed it. Example: with a 20K chat model
at 70% and an 8K override, main compacts at 5,600 tokens; this PR
compacts at 6,400 (80% of 8K, shown as 32%).

## Changes

Backend (`coderd/x/chatd`):

- The trigger with the lower token point binds (ties go to the chat
trigger). It drives the automatic compaction gate and the
still-over-limit check, and every summary, automatic or manual, records
it.
- An override that is unset, deleted, disabled, on a disabled provider,
or without credentials for the chat owner is treated as absent: the chat
model summarizes and only the chat trigger applies.
- The summary's output cap is bounded by the summarizer's own window,
not the trigger's.
- The still-over-limit error names what can clear it: the compaction
threshold (chat trigger), an override model with a larger window
(organization trigger), or both when usage is over both points.
- The still-over-limit check compacts once more when the latest summary
was recorded under a different threshold or trigger window. The
`chat_summarized` result gains `trigger_context_limit_tokens`, and
`context_limit_tokens` now records the chat model's window (main
recorded the smaller window). On main, lowering a threshold after a
compaction ended every later turn with the error.
- The still-over-limit check skips a 0% threshold ("always compact"),
which on main ended the run with the error after every compaction.
- `ARCHITECTURE.md` documents the two triggers, the summary cap, and the
still-over-limit rules.

Frontend (`site`), through a shared `compactionTriggers` helper that
mirrors the backend rules (no new API):

- Coder Agents settings: the compaction override row warns when the
organization trigger undercuts chat models' default triggers ("the
organization override compacts chats at 80% of {model}'s {N}-token
window").
- Personal thresholds page: an Effective column shows the trigger that
applies, with a popover when the organization override binds. "Compacts
at" follows the same trigger, and a point the chat model's window cannot
reach shows as Off. The header's 100% sentence, and the row warning when
a member types 100, say an organization override may still compact chats
when one is set or its organization's overrides failed to load (the
header also while they load).
- Chat context gauge: the compact-at percent follows the binding trigger
and is annotated when the organization override binds. If the overrides
request fails, it shows the chat-only value with "(organization override
not loaded)", or "Compaction off (organization override not loaded)"
when the chat trigger is off.
- An override whose provider the user cannot use is ignored on the gauge
and the personal page, matching chatd.
- The shared `formatContextLimit` prints counts below 1,000 exactly (an
800-token compaction point showed "~1K") and 999,500 to 999,999 as
"1.0M" instead of "1000K". It also labels the model pickers; their other
values are unchanged.
- `HelpPopoverIconTrigger` lets callers set their own `aria-label`; only
the Effective column's popover does.

Known v1 limits: the Coder Agents settings alert is organization-scoped,
so it can warn about an override that some members fall back from for
lack of provider credentials. The gauge checks the viewer's provider
access, so on a shared chat its label can differ from the owner's
runtime trigger (Codex P2, declined). Compaction itself is unaffected.

## Screenshots

From remote UAT round 13 on 093076f402: an isolated test deployment
with a local fake LLM. The models are test fixtures named for their
context windows: Chat model 20K (own threshold 70%), Chat model 5K
(40%), and Chat model 8K (50%). Chat model 8K is also the organization
compaction override, so the organization trigger is 80% of 8,000 = 6,400
tokens, which is 32% of Chat model 20K's window. Chat model 8K's own 50%
applies only to chats that use it as the chat model; changing it to 10%
or 100% left every value below unchanged.

**Coder Agents settings:** the organization trigger undercuts Chat model
20K's 70% default.

![Override warning: 80% of Chat model 8K's window undercuts Chat model
20K](https://github.com/user-attachments/assets/188c92e6-faaa-4e9a-a563-567cbf98af85)

**Personal thresholds:** the organization trigger binds Chat model 20K
at 32%. Chat model 5K (40%) and Chat model 8K (50%) keep their own chat
triggers.

![Effective column: 32% from the organization trigger on Chat model
20K](https://github.com/user-attachments/assets/9949be4e-9417-4cfc-a683-f1367a1dd77a)
![Organization override popover: 80% of Chat model 8K's window, about
32% of this model's
window](https://github.com/user-attachments/assets/4bc3e002-0be1-46fd-91f0-aa27503034af)

**Personal thresholds, organization settings request failed** (the first
red line is the injected error). Rows fall back to chat-only values.

![Organization settings load failure above usable chat-only
rows](https://github.com/user-attachments/assets/dcf48546-aa47-4f82-a6eb-85f106b4bf45)

**Chat context gauge:** organization trigger binds (Chat model 20K),
chat trigger binds (Chat model 5K), overrides request failed (injected).

![Gauge: Compacts at 32% (organization
override)](https://github.com/user-attachments/assets/cd5402d9-9849-4254-a7a7-6623c4006a2a)
![Gauge: Compacts at
40%](https://github.com/user-attachments/assets/f372c9e9-72ef-41c8-950c-e77d74965f65)
![Gauge: Compacts at 70% (organization override not
loaded)](https://github.com/user-attachments/assets/ab829746-2ab5-4743-848f-dce4e806de3a)

**Still-over-limit errors from real runs:** organization trigger, chat
trigger, both.

![Organization-only over-limit
error](https://github.com/user-attachments/assets/f210807a-a2cc-47a0-807c-f1404373bb28)
![Chat-only over-limit
error](https://github.com/user-attachments/assets/eecec38e-97de-49b6-8670-b32090103bbb)
![Combined over-limit
error](https://github.com/user-attachments/assets/459a6d08-7215-492d-93f5-96a4f767a09e)

## Testing

- Go: `TestBindingCompactionTriggerSource`,
`TestCompactionStillOverLimitError`, `TestCompactionStatusFromHistory`,
and `TestActiveServer_CompactionModelOverride` cover the 80% point
(fails if the override's own threshold is read again), user threshold
100 leaving the organization trigger on, the summary cap for automatic
and manual compaction, the over-limit messages, and recompaction after a
trigger change. The site binding tests mirror the Go table row for row.
- Site: `compactionTriggers` unit tests and Storybook stories for the
three surfaces, including the overrides fetch failure.
- On c6f8750479: `go vet`, the `chatd` and `chatloop` tests, and the
AgentsPage, AISettingsPage, and HelpPopover unit tests (2,274 passed, 2
skipped) pass, and CI is green. On 987cefe399, tsc, the five affected
story files (109 passed), and `make pre-commit` also passed. 093076f402
only changes `ARCHITECTURE.md`; the pre-commit hook's markdown format
and lint checks pass on it.
- On 3cd5b08f1f: tsc, the new `formatContextLimit` test (fails on the
old helper for 800 and 999,999), the `compactionTriggers` and `aiModels`
unit tests (224 passed), and the affected story files pass, and the
pre-commit hook passed on all three commits.

## Review record

- Earlier heads: 12 Codex passes and 3 human reviews, all threads
answered. Rebased onto main several times, most recently ff3fa22f68
(kept #30274, #29892, and #29890 in conflicted files).
- coder-agents-review rounds 1 to 18: findings fixed; approved since
round 15. The round 1 `ARCHITECTURE.md` finding was left for the
maintainer, who asked for the update in the last commit.
- coder-frontend-review (eight roles): all pass. Splitting the PR (RO6)
was declined because it is reviewed as one unit; the
`organizationTriggerFor` story helper stays because its three fields
must match the model.
- Design change after UAT round 11: the organization trigger is a fixed
80% of the override window instead of the override model's own threshold
(171d5f568e, 21d2ce7fce, 987cefe399).
- coder-agents-review round 19: copy, naming, and an unreachable
zero-window gate fixed (6e8efb9054, c6f8750479). A summary-time fit
check with chat-model fallback (CRF-156) was declined: the previous-step
lag applies to every trigger, including main's. Round 20 approved
c6f8750479 with no findings.
- Codex: one P2 on 987cefe399 declined (see known limits); no major
issues on c6f8750479.
- Remote UAT on dogfood (fake LLM, isolated test deployment): rounds 8
to 13 PASS. Round 12 on c6f8750479 first failed one criterion that was
written wrong (the admin alert correctly lists an override model that is
also a chat model at 100%); the reviewer endorsed PASS on the same
evidence after the correction. Round 13 on 093076f402 retook the
screenshots with neutral model names and confirmed that the override
model's own threshold (10%, 50%, 100%) does not move the organization
trigger.
- UAT round 13 notes: the header after a failed or pending overrides
load and the sub-1K labels are fixed in 4e172ab5ff and d5888ce15dc;
coder-agents-review round 21 found the same gap in the row warning
(CRF-167, fixed in 3cd5b08f1f). Left as on main: a summary's
`usage_percent` is measured against the binding trigger's window, so it
can exceed 100 (nothing reads it), and the 100% help text shows only for
an unsaved edit.
- Codex: no major issues on d5888ce15d and 3cd5b08f1f.
coder-agents-review round 22 approved 3cd5b08f1f with no open findings.
- coder-frontend-review on those commits: all roles pass after the
fixes. Declined: moving the formatter fix to its own PR (RO5; the owner
asked for it here), adding "tokens" to sub-1K labels (TN6; "~800" reads
like "~16K"), and splitting the component (RP6).
2026-10-11 12:10:59 +02:00