Woken chat page comes back as it was left
Follow-up to #20162. A hidden chat whose page hibernation released now comes back as it was left. Before this, waking it dropped three things the page held and acpmux did not:
- composer attachments
- shell mode's unrun command
- the transcript's scroll position
Also, while the acpmux turn watch is down, no chat's turn is known, so every chat counts as busy and keeps its page.
What changed
- Before a hidden chat page lets go, hibernation asks the page for
pane.wakeState. The answer holds the composer's file chips, shell mode and its text, and the transcript's last visible place. Shell-run and move chips are left out: a page with shell runs is never released. The tab keeps this state in memory (AgentTabStore.wakeStates). - The page built when the tab shows again gets that state once, in its handshake (
wake). The page puts the chips and shell text back into the composer and opens the transcript at the kept place. It also saves that place as the chat's scroll anchor, so a relaunch agrees with it. - The transcript's place is read from where the reader last saw it, not the hidden viewport. A scroll that settled after the pane hid was never reported before (
anchorDeferred), so a fresh page opened at the older anchor. - The transcript's place is the anchor last measured while the page was in view. When it was never in view (a chat left before its rows arrived) or a subagent's view is open, the place is unknown. The wake then leaves it out, and the chat's kept anchor stands. At the latest message it says
atLatest. - A connected page that doesn't answer
pane.wakeStatewithin 3 s keeps its page (a large attachment still encoding). The next plan asks again. A page that never connected holds nothing and goes as before. - The wake goes only to a fresh page's
ready, not areconnect. If the page build fails, the tab keeps it. -
AgentTurnStateStore.watchingis true from the watch's first page until it closes. While it is false,keepsPageanswers.agentTurnsUnknown(kept, like a busy chat). A run with no watch at all (showcase, the mock pane) has no turns to wait for, as before.followTurnsobserves it, so an overdue idle page goes when the watch comes back.
Testing
Both tests are in AgentPageHibernationTests, run with scripts/ci/run-swift-testing-suites.sh Packages/macOS/CmuxNext on a fleet mini:
aWokenChatComesBackAsItWasLeft: releases a hidden chat. The rebuilt page'sreadyhandshake must carry the attachments, shell text and scroll anchor, exactly once.withTheTurnWatchDownEveryChatKeepsItsPage: with the watch down, a chat overdue by an hour must keep its page (.agentTurnsUnknown). Once the watch is back, the page must go.
Results:
- Red at 21ddc296cdce: both fail. The page was never asked (
asked.count โ 0), the handshake had nowake, and the chat with the watch down was released. - Green at 2cd8f1da6944: both pass; the suite's only failure is the same
KeybindingsFileRelaunchTestscase.
KeybindingsFileRelaunchTests (Ctrl-Tab conformance vectors) failed in the red run too. This PR doesn't touch keybindings.
bun run check and the composer and UI-rules bun tests pass in webviews/.
Not verified live in a running build: the page side, meaning pane.wakeState and the composer and transcript restore. Only the Swift handoff is under test.
Changelog
Fixed: A chat that was asleep in the background now comes back with its attachments, unrun shell command and scroll position.
๐ค Generated with Claude Code