Files
coder/site
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
..
…
…
…
…
…
…
…