fix(desktop,ui): a failed read reads as a quiet line or an empty place's state, not a red sentence
Implements the plan Audit error state UIs and improve them (https://tryluke.dev/stage/plan/02fe53cc-bb22-4861-abf1-995ec3225a3d).
Summary
A read that does not land ("Your tasks could not be read.") was drawn as a bare red sentence with Retry after it. A network blip looked like something broken for good, and a failure alone in a whole page or tab stood as a red line near its top. #3242 has since moved this into @luke/ui's ErrorState, so this PR changes that one component, which every failed read in the window draws through, and fixes the call sites the audit found.
Among rows (the sidebar, Home's Recent and Inbox, the Inbox list, Settings cards, under a transcript):
- The sentence is now in the muted ink of the words around it, and a red alert glyph alone marks the fault. The glyph is a new
AlertIcon, Lucide'scircle-alertdrawn in our stroke. - Retry stands beside the sentence, or under its first word where the line has no room for both, as in the narrow sidebar. The glyph stays centred on the sentence's line either way.
Alone in a region (Factory, Automations, Issues, a task, the agent page, the Changes tab, page source, transcripts, version history, the plan template):
- It is now that region's
EmptyState: the glyph large and faint, the sentence, and Retry with its glyph, a little above the middle. EmptyStategains two optional props for this.alertmakes its body an ARIA alert, so a reader still hears the failure land.action.icongives its button a glyph.
Call sites from the audit:
- Home's Inbox line and the person page's two failure lines drew their own
div+RetryButton. They now useErrorState. - The agent page's list failure stood inline in a padded box that was otherwise empty. It is now centred, like the
NotFoundbeside it. - The automation runs panel's failure is now centred, like its own "No runs yet".
- The Changes tab, page source, and both transcripts wrapped the centred failure in
side-panel-empty/ConversationEmptyState, which doubled the padding. They now return it directly, as their empty states do. - Dead CSS is removed: the Factory-only margin rule and
.agent-page-note.
Left as they are: form and action errors (.error-message, such as a refused save or a field's problem). They answer something the developer just did, so red at the point of the act is right.
Evidence
Rendered from the real renderer components and styles/index.css in Chromium, over tasksControl({ listFailed: true }) and reads that answer UNANSWERED. The font is Linux's, not SF.
| Before | After |
|---|---|
![]() | ![]() |
![]() | ![]() |
![]() | ![]() |
pnpm check:affected: exit 0. Lint passes. Tests pass: ui 46 files / 250 tests, desktop 190 / 1695, web 48 / 182.
Invariants
-
./scripts/check.shgreen. (CI) - JSON Schema goldens unchanged.
- Every behavior change has, in this PR, the test that fails without it:
error-state.test.ts: each placement's fault glyph is hidden from a reader.empty-state.test.ts: a place a failure left empty is one alert holding its line and its Retry, and an empty place is no alert.read-failure.test.ts: its helper now reads an alert's words with the presses taken out, however they nest.
- No new
astype assertion. - No
Effect.run*outside the edges. - No new raw async primitives.
- No discarded Effect.
- Test count: +4 (two in
error-state.test.ts, two inempty-state.test.ts).desktop-factory.test.tsnow asserts on the "No agents at work" words rather than on.luke-empty-statebeing absent, since the centred failure is itself one. - No hand-rolled file replaced.
- No
AGENTS.md/CLAUDE.mdsentence names a changed part. -
PRIVACY.mdunchanged. - Package deps unchanged.
- Barrel/door rule unaffected.
Notes
- Blockers or follow-up verification: none. CI is Linux only, so how this looks on the Mac (SF type, the window's real widths) has not been checked by eye.





