fix(desktop,host): say small panels are reading, or refused, rather than empty

Merged
DA
16d ago
+639-51

PR 5 of 9 in the loading-states rollout (Loading states audit, plan Fix Inbox loading state, "Loading states rollout"). It covers the small panels that said there was nothing, or drew nothing, before their data arrived.

Summary

  • Side panel, Code tab. While the task's code is unread, the tab shows a SpinnerRegion ("Loading code"). It says "No code yet." only when the code is read as []. To make that distinction real, compose-tasks.ts's readWork now writes shownCode: [] when the read finds no code. Before, it left the field absent, so undefined stood for both "unread" and "none". withoutActivity still clears it when you switch tasks.
    • With that change, taskHolds reads a task with no code as holding none (false) rather than unknown. A first snippet shown later now arrives at the strip's end, like any other kind read as empty. use-side-panel.ts's arrived() comment no longer says code is left absent.
    • sidebar-pinned-tab.tsx's pinGone treats [] the way it treated the old absent value. A file pinned on a task Luke showed no code on was browsed to, not shown, so it isn't dimmed. Its behaviour is unchanged.
  • Automation editor, MCP servers row. While useIntegrationStatuses hasn't answered, the row shows a skeleton line for its description and a skeleton chip, inside a SkeletonRegion ("Reading your connected servers…"). It no longer claims "None connected yet." or shows a "None" chip. A refused read shows an ErrorState with Retry.
  • ⌘K. When the list is empty, the palette now says why instead of "Nothing matches.":
    • Tasks: "Reading your tasks…" until listRead, or a failure line with Retry (onRetryList) when listFailed.
    • People: "Reading the workspace's members…" until the members answer, or a failure line with Retry when the read is refused.
    • All: the first kind still reading, else the first one refused. A query under All no longer says "Nothing matches." while the repository's files are being read.
    • To support People, use-workspace-members.ts gains useWorkspaceMembersRead, which returns { members, refused, retry }. useWorkspaceMembers is now a thin wrapper over it, so its other callers are unchanged.
  • Terminal tab, Shells list. While the first list read is out, the list shows skeleton rows shaped like a shell's row ("Reading the shells…"). A refused read, which leaves the tab unreachable, shows an ErrorState ("The shells could not be read."). It has no Retry of its own because the screen's bar beside it already offers one. A sandbox that stopped before the list answered says "No shells.".

Evidence

  • Platform-independent checks: pnpm check:affected green locally (lint, knip, discarded-effect, the host and desktop vitest suites).
  • Each new test fails with its source change reverted (checked one by one):
    • desktop/desktop-tasks.test.ts: the Code tab waits while unread and says "No code yet." only for [].
    • packages/host/src/compose-tasks.test.ts: code read as none is held as [], and a work read that didn't answer leaves it unread.
    • desktop/automations-page.test.ts: the MCP row claims nothing while reading, and a refused read says so, then Retry reads it again.
    • desktop/command-palette.test.ts: All with a query while the files are read, People reading, refused, and retried, and Tasks reading and refused with Retry.
    • tasks/terminal-tab.test.ts: skeleton rows while the shells are read, and a refused read said in the list.

Screenshots (dark, from the jsdom suites' markup over the built stylesheet) follow in a comment.

Invariants

  • ./scripts/check.sh green. (CI)
  • JSON Schema goldens unchanged (LUKE_UPDATE_FIXTURES not run).
  • Every behavior change has, in this PR, the test that fails without it; no change-detector test added.
  • 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.
  • No new setTimeout/setInterval/new Promise/AbortController/fs.watch outside the recorded files. The new new Promise calls are in test files, which hold answers open.
  • No Effect, Stream, or Layer written as a statement and dropped.
  • Test count delta: +10 (automations-page 2, command-palette 4, terminal-tab 2, desktop-tasks 1, compose-tasks 1).
  • No hand-rolled file replaced.
  • No CLAUDE.md/AGENTS.md sentence names a changed part.
  • PRIVACY.md unchanged.
  • Package deps unchanged.
  • Barrel/door rule untouched.

Notes

  • Follow-up: a refused work read keeps whatever was held, and TasksView carries no failure marker for it. So on a refused read the Code tab keeps spinning until the next refresh or open reads the work again. Saying the read was refused would need a failure field in the TasksView wire schema. That's left out of this PR, as agreed with the orchestrator.
  • compose-tasks.ts, use-workspace-members.ts, use-side-panel.ts (a comment) and sidebar-pinned-tab.tsx are outside the files this PR was first given. The orchestrator approved the first two, and no other PR in the rollout owns any of them.

Made with Luke


Devin Review


Summary by cubic

Stops four small panels from claiming they are empty before their data arrives. Each now shows a reading state while a request is in flight, and says a read was refused instead of drawing nothing.

  • The Code tab's spinner now distinguishes unread from "No code yet.": the host writes a task's shown code as [] when the read finds none, so undefined alone means unread.
  • The automation editor's MCP servers row shows a skeleton while reading and an ErrorState with Retry when refused.
  • ⌘K's Tasks and People filters say they are reading until their lists answer, and offer Retry when refused; under All, the first still-reading or refused kind explains an empty list.
  • The Terminal tab's Shells list shows skeleton rows while reading and an ErrorState when refused.