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
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
--dangerwhen 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.publishedfrompull_request/check_suite/check_runevents. No polling or sweep was added. PlanCardis built and tested here but not yet hung on the rows: the plainTooltiptakes only a string label, and #2049, which adds itscardprop, is waiting on another PR. A small follow-up wiresPlanCardonto 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 carriesagentsfor 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 throughplanListAnswerSchema.apps/web/tests/coding-agent-store.test.ts:publishedis 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'sagents.apps/desktop/src/main/agent-notices.test.tsandipc/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 optionalagentskey.
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:
| Light | Dark |
|---|---|
![]() | ![]() |
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

