fix(desktop): a page's empty state fills the room under its header and stands a little above its middle

Merged
DA
4d ago
+118-39

Summary

Dean, with a screenshot of the empty Automations page: "I feel like the empty state should be centered. Can you fix this and all other empty states that make sense."

Cause. EmptyState centred itself with margin: auto, which only works when its parent is a flex column that fills the pane. That was true for the Inbox pane, the side panel, and a plan's transcript, so those centred, though at the true middle. The Automations scroller is a block box, so its empty state sat at the top. Settings › Experimental put it in a fixed min(420px, 60vh) box, so it sat wherever that box ended.

Fix, once, in the shared component. EmptyState (apps/desktop/src/renderer/empty-state.tsx) is now a .empty-state box that:

  • fills the room its pane leaves under the pane's header (flex: 1);
  • centres its glyph and line horizontally;
  • places them vertically at the optical centre: two spacers share the free room 2:3 above and below, which puts the state's middle about 41–43% of the way down;
  • scrolls inside the pane when the window is shorter than the state. The spacers shrink to nothing, so the state never runs under the header.

Three panes scroll a list as a block box. Each lays itself out as a column while it holds an empty state: one :is(…):has(.empty-state) rule beside .empty-state in desktop.css. They are .automations-scroll, .inbox-plans-scroll, and .settings-page-scroll. The one-off .settings-empty box is deleted. There are no per-page margins.

PlaceBeforeAfter
Automations, no automationsTop of the pane, ~60px under the headerOptical centre below the header
Automations › an automation that's goneTop of the paneOptical centre
Settings › Experimental, no experimentsMiddle of a 420px box under the page titleOptical centre of the page below the title
Window Inbox, nothing waiting (two columns)True centre of the item paneOptical centre of the item pane
Window Inbox, one-column tierA grey "Nothing needs you" note at the top of the listThe Inbox's empty state (glyph and line) centred in the list
A plan's Inbox tab, one-column tierSame note at the top of the listSame as the window Inbox
A new automation's / an automation's Runs panel, no runsA note under "Runs"Clock glyph, "No runs yet", and how to have one, centred in the panel
Side panel Code, an agent's Changes, a plan's TranscriptTrue centreOptical centre (same component)

Left alone, on purpose:

  • Inline notes inside a list or among other content: the sidebar's "No plans yet.", "Nothing needs you" under Home's waiting section, Settings search's "No results", pickers' "No matching skills", the terminal shells list's "No shells.", and find-in-tab's "No results".
  • Factory empty: "Nothing is happening right now." and the New plan prompt sit under the Plan → Build → Ship cards. The cards are the page's content, so the prompt belongs under them rather than in the middle of the window.
  • Home with nothing waiting: Home's composer and sections are its content; "Nothing needs you" is a line in one of them.
  • The planless agent page: before any message it shows the agent's starting line, a wait rather than an empty state.
  • Waits and failures (spinners, ReadFailure) and .desktop-empty / .side-panel-empty: the plan that can't be drawn, "Choose a plan", a version history with no history, a file or page that can't be read. These are already vertically centred in their panes, and the component's own doc says a wait or a failure isn't empty.

RENDERS

The images are the real DesktopShell and stylesheet, built with esbuild into a small harness page and rendered by headless Chrome on Linux at 2×. The data is fixture data: workspace Acme, Automations and Factory experiments on, an empty automations list, and an empty Inbox. The Experimental page is drawn with its experiment list emptied.

Evidence

  • ./scripts/check.sh: exit 0 (repository contracts, typecheck, lint/knip/discarded-effect, every vitest project, builds)
  • New and changed tests:
    • desktop-inbox.test.ts: an Inbox with nothing in it shows its empty state in the pane beside the list, and in the list itself once the list stands alone.
    • desktop-plans.test.ts: a plan's Inbox tab in the one-column tier shows the empty state in the list.
    • automations-page.test.ts: the Runs panel with no runs shows the empty state ("No runs yet" over "Run now to try it.").
    • All three fail on main and pass with this change. The existing EmptyState tests in experimental-page.test.ts and the side-panel tests pass unchanged.
  • Filling and centring are layout. jsdom lays nothing out and the repository has no browser test layer, so the renders above are the evidence for them. They include a window 300px tall and one 220px tall, where the state starts below the header and scrolls in its pane.
  • Mac: CI runs only on Linux and couldn't check the Mac. I didn't run the Electron app. What I checked was the shell's DOM (jsdom tests) and its rendering (headless-Chrome images above). The change is CSS and React markup only; nothing in main, preload, or the host is touched.
  • Test count: +1 test (desktop-inbox.test.ts). Two existing tests gained an assertion.

Invariants

  • ./scripts/check.sh green. (CI)
  • JSON Schema goldens unchanged (LUKE_UPDATE_FIXTURES not run, or diff explained line by line). (CI)
  • Every behavior change has, in this PR, the test that fails without it; no change-detector test added. (manual; root AGENTS.md "Testing")
  • Gateway envelope goldens unchanged. (CI)
  • No new as type assertion outside the allowlisted files. (CI via anti-slop)
  • No Effect.runPromise/runSync/runFork outside the runtime edges and the shims tools/anti-slop/effect-edges.json records. (CI via anti-slop/no-run-promise-outside-edges)
  • No new setTimeout/setInterval/new Promise/AbortController/fs.watch outside the files tools/anti-slop/effect-edges.json records. (CI via anti-slop/no-raw-async-primitives)
  • No Effect, Stream, or Layer written as a statement and dropped; a description a caller walks past runs nothing. (CI via pnpm discarded-effect)
  • Test count equals the pre-PR count or the delta is listed. (manual: vitest run --reporter=json)
  • Each hand-rolled file the PR replaces is deleted, or the part that remains is a permanent adaptor named in root AGENTS.md's "The permanent adaptors" with the reason it stays.
  • Every CLAUDE.md/AGENTS.md sentence naming a changed part is edited; root pair stays byte-identical. (CI for pair existence; manual for wording)
  • PRIVACY.md unchanged, or the change is a product decision called out in the PR body.
  • Package package.json deps match what the sources import by bare specifier; effect via catalog:. (CI)
  • Barrel/door rule: no Node-reaching Effect module (@effect/platform-node, effect/sql) behind a barrel the renderer or a web function opens. (CI: existing renderer node: grep plus a new grep for those specifiers in renderer bundles)

Notes

  • Blockers or follow-up verification: please glance at the empty Automations page and Settings › Experimental on the Mac.

🤖 Generated with Claude Code


Open workspace in Conductor


Devin Review


Summary by cubic

Centers empty states in desktop panes so they fill the room under the pane's header and sit slightly above its middle, instead of hugging the top or landing in a fixed box.

  • The shared EmptyState now fills its pane and stands its glyph and line at the optical center, scrolling when the window is shorter than the state.
  • Automations, the one-column Inbox list, a plan's Inbox tab, an automation's empty runs panel, and Settings › Experimental now use this centered state instead of a note or a fixed-height box.
  • Inline notes inside lists, waits, and failures are left as they were.