The studio draws an app as a row in exactly one place, and that place is sixty lines in the middle of studioHome.
src/web/studio/home.ts:74-131 builds each row inline: the production environment's state, the latest run read through runWord, who ran it, how many asks the app has, the tone, and the row({ … }) itself. It is good code in the wrong file — home.ts is one of the busiest files on this branch, and #71 (/apps is the shelf) was parked because main moved under it eight times and the branch could not be brought up to date, conflicting on this very file. The public shelf (studioLanding, src/web/pages/site.ts:536) already draws its own tiles a second way, and a third page wants the same row.
Pure extraction, no behaviour change: this is the move that lets the /apps shelf (the rewrite of #71) land in one run instead of losing a race with main.
Acceptance
[ ] A new src/web/studio/shelf.ts exports one function that takes the viewer's context and a list of app repositories and returns their rows — the gathered reads (latestAgentRunsForRepos, getUsersByIds, decisionsFor, runtime.status) move with it, so the caller hands over repositories and gets rows.
[ ] studioHome calls it and is that much shorter; the Your apps card, its head, its buttons and its empty state stay exactly where they are.
[ ] The rendered bytes of / in studio mode are unchanged for a viewer with apps — a test that snapshots the app row before and after would pass either way.
[ ] The module's doc comment says what a shelf row is: name, one line, whether it is live, what the crew last did, the asks pill, the way in.
[ ] bun test green, and the existing studio home tests still cover the row through the new function.
Files I expect to change
src/web/studio/shelf.ts (new), src/web/studio/home.ts, test/studio.test.ts if a test names the old private shape.
Risks
runWord is async and reads git, so the rows are gathered with Promise.all: keep that, and keep the reads grouped — one query for the latest runs of every app, not one per app, exactly as today. Who sees what must not move: the ownerFacts check that hides who ran it travels with the row.
The studio draws an app as a row in exactly one place, and that place is sixty lines in the middle of
studioHome.src/web/studio/home.ts:74-131builds each row inline: the production environment's state, the latest run read throughrunWord, who ran it, how many asks the app has, the tone, and therow({ … })itself. It is good code in the wrong file —home.tsis one of the busiest files on this branch, and #71 (/apps is the shelf) was parked because main moved under it eight times and the branch could not be brought up to date, conflicting on this very file. The public shelf (studioLanding,src/web/pages/site.ts:536) already draws its own tiles a second way, and a third page wants the same row.Pure extraction, no behaviour change: this is the move that lets the
/appsshelf (the rewrite of #71) land in one run instead of losing a race with main.Acceptance
src/web/studio/shelf.tsexports one function that takes the viewer's context and a list of app repositories and returns their rows — the gathered reads (latestAgentRunsForRepos,getUsersByIds,decisionsFor,runtime.status) move with it, so the caller hands over repositories and gets rows.studioHomecalls it and is that much shorter; the Your apps card, its head, its buttons and its empty state stay exactly where they are./in studio mode are unchanged for a viewer with apps — a test that snapshots the app row before and after would pass either way.bun testgreen, and the existing studio home tests still cover the row through the new function.Files I expect to change
src/web/studio/shelf.ts(new),src/web/studio/home.ts,test/studio.test.tsif a test names the old private shape.Risks
runWordis async and reads git, so the rows are gathered withPromise.all: keep that, and keep the reads grouped — one query for the latest runs of every app, not one per app, exactly as today. Who sees what must not move: theownerFactscheck that hides who ran it travels with the row.Suggested: Scout.