feat(web,desktop): sidebar plan rows mark an agent at work or a pull request, and hang a card with the plan's agents and pull requests

Merged
DA
8d ago
+1980-38

Dean: "Let's add some indicator of if agents are working or if there is a PR done in the plan row in the sidebar… don't add too much. We can also add more information in a hover element on a plan in the sidebar."

What changes

Sidebar row: one quiet mark, no dots. The mark goes just before the relative time and steps out with the time while the row's Pin/Archive buttons show:

  • an agent is starting or running on the plan → a small spinner in the time's muted ink (it stays still under reduced motion, via --loop-motion)
  • else the plan has an open or draft PR → a pull-request glyph, GitHub green when open and muted when draft, tinted --danger when checks fail
  • else a merged PR → the merged glyph in the merged purple
  • else (closed PR or none) → nothing

Plan hover card (desktop/plan-card.tsx): the plan's name with how long ago it was updated, its repository, then up to three agent lines (title + ModelLabel, "Working… 2m 5s" / "Done 3m" / "Failed 1h" / "Stopped …"), then up to three PR lines ("#128 · title", +/− coloured, Draft/Merged/Closed). Each list ends with "and N more" when it was capped. It is built only from the list row, so nothing is fetched on hover.

Data. planSummarySchema gets an optional agents field (planAgentsSummarySchema: total, working, ≤3 agent lines, PR total, ≤3 PR lines), answered only for plans that have agents. GET /api/plans computes it in three statements for the whole list (hosted/plan-agents.ts): the agents of the listed plans, their newest turns, and the lines still awaiting a turn. Status is read exactly as the agent tab reads it (coder-host/status.ts). GitHub is never asked on the list path. PR state comes from a new nullable column, coding_agent.published (migration 0078), written on every successful read of what an agent published (coder-host/published.ts; this covers the agent tab's poll, the end-of-turn notice, and the voice tool). LatestTurn now carries settledAt for "Done 3m". The host keeps a row's agents when a pin or archive answer replaces the row.

Refresh. Main's agent-notices ledger asks for the plan list again (planningRefresh) when it sees an agent start or a turn end, and when a read of what an agent published finds something different from the last read for that agent. Repeated identical reads do not trigger a refresh. The list also still refreshes on its normal triggers.

Follow-ups

  • A PR merged on GitHub changes in the sidebar only once something reads it again (opening the agent's tab, or a turn's end notice). Dean chose GitHub App webhooks to close that gap; that is a separate PR stacked on this one, which updates coding_agent.published from pull_request / check_suite / check_run events. No polling or sweep was added.
  • PlanCard is built and tested here but not yet hung on the rows: the plain Tooltip takes only a string label, and #2049, which adds its card prop, is waiting on another PR. A small follow-up wires PlanCard onto each row through that prop once #2049 lands, so the screenshots show the card as it will look, drawn beside its row.

Tests

  • apps/web/tests/plans-app.test.ts: the list carries agents for a plan with agents (working first, ended ones with their settled time, the PR counted once across two agents, a past-grace agent counted but not named) and nothing for a plan with none; it round-trips through planListAnswerSchema.
  • apps/web/tests/coding-agent-store.test.ts: published is recorded and read back beside the agents of the plans named, and nothing of another account's is read or written (PGlite).
  • apps/web/tests/coding-agents-app.test.ts: every PR a published read finds is kept on the row, and a later merge overwrites it.
  • packages/host/src/compose-planning.test.ts: a pin keeps the row's agents.
  • apps/desktop/src/main/agent-notices.test.ts and ipc/coding-agent-acts.test.ts: the list refresh fires on a start or an end and on a new or changed PR, and not on repeats.
  • desktop-sidebar.test.ts: the row shows a spinner while working, an open/draft/merged glyph for those PRs, and nothing for a closed PR or no agents.
  • plan-card.test.ts: card lines from a fixture.
  • Wire goldens re-recorded with LUKE_UPDATE_FIXTURES=1. The diff only adds the new schemas and the optional agents key.

Screenshots

Rendered from the real renderer components and stylesheet in headless Chrome, with a fixture sidebar in mixed states and the card shown beside its row:

LightDark
lightdark

Mac verification

CI could not verify the Mac UI, and ./scripts/verify.sh was not run: this was built on a Linux cloud VM, and CI builds nothing for the Mac. LUKE-159 is the manual Mac pass. ./scripts/check.sh exits 0.

🤖 Generated with Claude Code