Agent chat: the last turn's Copy row shows at rest when it ended on work, 8 under the run
Leo, 2026-11-02: "this spacing would be good + the latest copy buttons auto showing when the turn is done like chatgpt... my current nightly has it different". Earlier the same day: "i should be able to see the copy buttons on your responses when your things end", "those buttons should always just be there left aligned", and "too much spacing between tool calls".
What was wrong
A turn that ends on a reply already shows its row at rest (#31503, #31609). The case still broken is the chat's last turn when the agent spoke and then ended on a run of commands, with no reply after it. Its footer has no answer text, so turns.ts isEmptyFooter calls it empty, and .acpmux-row--last-footer .cv-turn-actions--empty { display: none } (#24055) hides it. The result is no buttons, just a blank band between the run and the composer.
Regressed by #31609. It dropped !footerSays(row) from isEmptyFooter. Before that, the turn's "Worked for Ns" label kept the footer a real row. Once the label moved into the time's tooltip, the footer counted as empty.
Change
turns.ts: addedendsChatOnWork(row), which is true for the chat's last turn (retryable, so no prompt after it) when it has something to copy (turnCopy, #29740). Such a footer is no longer empty. It is a real 32px row (--grid-transcript-footer-control) with Copy (the whole turn), Fork and Pin. #31503'sacpmux-row--final-footerrule shows it at rest, and its first glyph is on the prose's left edge. Earlier work-only turns keep their hover-only footer. A turn of tool calls with no words has nothing to copy and stays empty.transcriptGrid.ts:rowMarkmirrors this so the row getsMARK_FOOTER_ROW.transcriptGap(MARK_STEP | MARK_STATUS, MARK_FOOTER_ROW)is now--grid-gap-footer(8px), the same as reply→row. Before this it would have been the 24px block gap. No new values: only the grid tokens from #27580/#31791 are used.- The other gaps already match ChatGPT on the tip, so they are unchanged: reply→row 8, row→next prompt 32 (turn gap), row→composer 24 (block gap), run↔prose 16 (#31879).
Mock scene
Gallery agent-pane.transcript-row-alignment, new variant finished-work: a finished Claude chat whose last turn says one line, runs six commands and ends.
Tests
TurnFooter.test.tsx: "the chat's last turn that ended on work has a row of its own, 8 under the run" and "a work-only last turn that said nothing has nothing to copy and stays empty". Both fail on base.test/reply-footer.test.ts(browser): "a finished chat's last turn shows its row at rest, 8 under its last line" runs forfinishedandfinished-work, in light and dark. It checks the row is at opacity 1 with no pointer, its first glyph is on the prose edge, the gap above it equals--grid-gap-footer, and the row's height equals--grid-transcript-footer-control(no extra band).ContinueAfterStop.test.tsx: the "nothing else to show" fixture now also clearsturnCopy.
Design spec: leo-workspace design/transcript-spacing.md ("Reply footer") and design/chat-transcript.md. The spec text for the turn-end row is going in with this PR's report.
Risk: small and contained to the last footer's emptiness and one gap rule.
Agent session: agent session
🤖 Generated with Claude Code
![Autofix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark-3x.png)
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Fixes the agent chat transcript so the last turn's Copy, Fork, and Pin row shows at rest when that turn spoke and then ended on a run of commands with no reply after it. Previously that footer was treated as empty and hidden, leaving a blank band over the composer.
- The footer is now a real 32px row shown at rest, 8px under the run's last line (same gap as under a reply) instead of the 24px block gap.
- Turns that ended on work but had nothing to copy stay empty, and earlier work-only turns keep their hover-only footer.