fix(desktop,ui): a failed read reads as a quiet line or an empty place's state, not a red sentence

Merged
DA
5d ago
+200-107

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's circle-alert drawn 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.
  • EmptyState gains two optional props for this. alert makes its body an ARIA alert, so a reader still hears the failure land. action.icon gives 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 use ErrorState.
  • The agent page's list failure stood inline in a padded box that was otherwise empty. It is now centred, like the NotFound beside 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.

BeforeAfter
Home beforeHome after
Factory beforeFactory after
Settings beforeSettings after
  • pnpm check:affected: exit 0. Lint passes. Tests pass: ui 46 files / 250 tests, desktop 190 / 1695, web 48 / 182.

Invariants

  • ./scripts/check.sh green. (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 as type 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 in empty-state.test.ts). desktop-factory.test.ts now asserts on the "No agents at work" words rather than on .luke-empty-state being absent, since the centred failure is itself one.
  • No hand-rolled file replaced.
  • No AGENTS.md/CLAUDE.md sentence names a changed part.
  • PRIVACY.md unchanged.
  • 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.