feat(desktop): show the empty state on an empty workspace's Issues board
Merged
DA
11d ago+93+93+01234567890123456789-20-20-01234567890123456789
Item "Empty board shows EmptyState" of the Improve Issues board plan. The developer settled the plan's open question: a workspace with no issues shows the app's empty state on the board too, instead of five empty columns.
Summary
issues-page.tsx:listStatenow givesCOLLECTION_STATE.EMPTYto an empty workspace in both layouts, not only the List.CollectionPagethen draws the sameEmptyState("No issues yet",ListTodoIcon) on the board. The board's reading blocks (IssueBoardSkeleton) are unchanged. A workspace with issues still stands all five columns, empty ones included.- The empty state's way on: a New issue button through
EmptyState'sactionslot. It is wired to the toolbar's ownonCompose, so it opens the same card (on Backlog). It is disabled whenever the toolbar's New issue is: no workspace listed, or the card already open. It matches the Inbox'sNeedsYouEmpty, a label alone with no glyph. Factory and Automations have no action in their empty states. "New issue" is now one constant used by both buttons. @luke/uiEmptyStateActiongains an optionaldisabled, passed to itsButton. Without it, the empty state's button would vanish and reappear behind the card each time the card opened.@luke/uiCollectionPage(CSS only): the notice wrapping an empty or failed state no longer adds its own 2:3 spacers around anEmptyState. TheEmptyStatealready does that split itself, so the two stacked. That left the state a sixth of the room (about 115px on an 800px window) and clipped anything past the icon and line: this New issue, and a centredErrorState's Retry. The rule is scoped to:has(> .luke-empty-state). With the spacers gone, a line-only state sits about 20px higher than before, at the 2:3 point of the whole room.
Shared component change
Two @luke/ui components owned by @charleslpan change.
EmptyStateAction.disabled(optional): passed straight to the action'sButton. Every existing caller (NotFound, the Inbox'sNeedsYouEmpty,ErrorState's Retry) leaves it out, so their buttons are unchanged.CollectionPage's notice spacers:.luke-collection-page-noticestands its content at the room's 2:3 point with::before/::after.EmptyState, and a centredErrorState, which is drawn as one, already does the same split inside aflex: 1 1 0box. Inside the notice, the two splits stacked: the state got a sixth of the room and scrolled what didn't fit. Onmainthat already hides Factory's Retry when its tasks could not be read (shots below), and it would have hidden this New issue. The new rule drops the notice's spacers only where its direct child is.luke-empty-state. Its callers:- Factory (empty and failed): the line-only empty state sits 2px higher, and the failed state's Retry now shows. See the pair below.
- Issues (empty and failed): this PR's change. The failed state's Retry now shows too.
- Web admin (
AdminPage, the accounts table, a day): these pass plain text, not anEmptyState, so:has(> .luke-empty-state)does not match and nothing moves. - Automations, the Inbox and Home do not draw on
CollectionPage, so nothing changes there.
Before (main) | After | |
|---|---|---|
| Factory, nothing at work | ![]() | ![]() |
| Factory, tasks could not be read | ![]() | ![]() |
Screenshots
These come from a throwaway, uncommitted jsdom test that rendered the real IssuesPage with an empty workspace. Headless Chromium then painted them at 1280×800 in .desktop-shell > main.desktop-main, with the sidebar folded, using the desktop stylesheet built from main (before) and from this branch (after). This is the same method as #3559 and #3642.
| Before | After | |
|---|---|---|
| Board, empty workspace | ![]() | ![]() |
| List, empty workspace | ![]() | ![]() |
| Board, empty workspace, light | ![]() | ![]() |
| List, empty workspace, light | ![]() | ![]() |
After filing the first issue, the board stands its five columns again:

Evidence
pnpm check:affected: typecheck, lint and knip pass, and the desktop and ui suites pass.discarded-effectran out of memory inside turbo's parallel group in this sandbox. Run alone, it passes ("No discarded Effect, Stream, or Layer in 2342 files"). The first run's one desktop failure wasapp-menu.test.ts, because the sandbox lacked Electron's binary. It passes once the binary is installed.issues-page.test.ts: the test "a workspace with no issue yet still stands the board's columns, each saying it holds none" is replaced by "a workspace with no issue yet says so on the board and in the list, its New issue opening the card, and the first issue filed stands the board's columns". The new test fails onmain("the empty state offers New issue"). The column-+ test now stands one issue first, since an empty workspace no longer draws column heads.empty-state.test.ts: new test "a way on unavailable for now still stands, and a press does nothing". It fails without thedisabledfield.- Test count: +1. The issues-page test was replaced one for one, and empty-state gains one.
Invariants
-
./scripts/check.shgreen. (CI) - JSON Schema goldens unchanged.
- Every behavior change has, in this PR, the test that fails without it; no change-detector test added.
- Gateway envelope goldens unchanged.
- No new
astype assertion outside the allowlisted files. - No
Effect.runPromise/runSync/runForkoutside the runtime edges. - No new
setTimeout/setInterval/new Promise/AbortController/fs.watch. - No Effect, Stream, or Layer written as a statement and dropped.
- Test count delta listed above (+1).
- No hand-rolled file replaced.
- No
CLAUDE.md/AGENTS.mdsentence names a changed part. -
PRIVACY.mdunchanged. - Package deps unchanged.
- Barrel/door rule unaffected.
Notes
- No hosted-service change, so no
run.sh --previewrun is needed. - Blockers or follow-up verification: None
Made with Luke











