Files
coder/biome.jsonc
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

137 lines
4.0 KiB
JSON

{
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true,
"defaultBranch": "main"
},
"files": {
// static/*.html are Go templates with {{ }} directives that
// Biome's HTML parser does not support.
"includes": ["**", "!**/pnpm-lock.yaml", "!**/static/*.html"],
"ignoreUnknown": true,
"maxSize": 2097152
},
"linter": {
"rules": {
"a11y": {
"useKeyWithClickEvents": "off", // oxlint owns click-events-have-key-events
"useMediaCaption": "off", // oxlint owns media-has-caption
"noSvgWithoutTitle": "off",
"useButtonType": "off",
"useSemanticElements": "off",
"noStaticElementInteractions": "off"
},
"correctness": {
"noInvalidBuiltinInstantiation": "off", // oxlint owns new-for-builtins
"useExhaustiveDependencies": "off", // oxlint owns exhaustive-deps
"noUnusedImports": "warn",
"useUniqueElementIds": "off", // TODO: This is new but we want to fix it
"noNestedComponentDefinitions": "off", // TODO: Investigate, since it is used by shadcn components
"noUnusedVariables": {
"level": "warn",
"options": {
"ignoreRestSiblings": true
}
}
},
"security": {
"noDangerouslySetInnerHtml": "off" // oxlint owns no-danger
},
"style": {
"noNonNullAssertion": "off",
"noParameterAssign": "off",
"useDefaultParameterLast": "off",
"useSelfClosingElements": "off",
"useAsConstAssertion": "error",
"useEnumInitializers": "error",
"useSingleVarDeclarator": "error",
"useConsistentCurlyBraces": "off", // oxlint owns jsx-curly-brace-presence
"noUnusedTemplateLiteral": "error",
"useNumberNamespace": "error",
"noInferrableTypes": "off", // oxlint owns no-inferrable-types
"noUselessElse": "error",
"noRestrictedImports": {
"level": "error",
"options": {
"paths": {
"react": {
"message": "React 19 no longer requires forwardRef. Use ref as a prop instead.",
"importNames": ["forwardRef"]
},
"@pierre/diffs": {
"message": "Parse diffs through parseDiffString so every rendered file carries a content-derived cache key (pierrecomputer/pierre#1052).",
"importNames": [
"parsePatchFiles",
"processPatch",
"processFile",
"parseDiffFromFile"
]
},
// "#/components/Stack/Stack": "Use Tailwind flex utilities instead (e.g., <div className='flex flex-col gap-4'>).",
"sonner": {
"message": "Import toast from #/components/Toaster/toast.",
"importNames": ["toast"]
},
"lodash": "Use lodash/<name> instead."
}
}
}
},
"suspicious": {
"noFallthroughSwitchClause": "off", // oxlint owns no-fallthrough
"noConfusingVoidType": "off", // oxlint owns no-invalid-void-type
"noExplicitAny": "off", // oxlint owns no-explicit-any
"noArrayIndexKey": "off",
"noThenProperty": "off",
"noTemplateCurlyInString": "off",
"useIterableCallbackReturn": "off",
"noUnknownAtRules": "off", // Allow Tailwind directives
"noEmptyInterface": "off", // oxlint owns no-empty-interface
"noConsole": "off" // oxlint owns no-console
},
"complexity": {
"noImportantStyles": "off", // TODO: check and fix !important styles
"noUselessThisAlias": "off",
"useLiteralKeys": "off"
}
}
},
"css": {
"parser": {
// Biome 2.3+ requires opt-in for @apply and other
// Tailwind directives.
"tailwindDirectives": true
}
},
"overrides": [
{
// parseDiff.ts is the single choke point allowed to import
// the raw @pierre/diffs parsers.
"includes": [
"**/DiffViewer/parseDiff.ts",
"**/DiffViewer/parseDiff.test.ts"
],
"linter": {
"rules": {
"style": {
"noRestrictedImports": "off"
}
}
}
},
{
// toast.ts is the single choke point allowed to import Sonner's toast.
"includes": ["**/components/Toaster/toast.ts"],
"linter": {
"rules": {
"style": {
"noRestrictedImports": "off"
}
}
}
}
],
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json"
}