Problem
packages/app/src/pages/session.tsx has grown into a 2,145-line session-page controller. It now owns route state, visible-session state, message history, timeline scrolling, composer dock layout, follow-up queueing, revert/restore, review mode, VCS diff loading, artifact files, desktop context sync, keyboard focus, mobile tabs, and right-panel wiring.
This makes local fixes risky because related state is split across distant blocks. The current composer dock scroll bug is the motivating defect: the timeline scroll container padding is written in one place, composer dock height is measured in two places, and only one path performs bottom-scroll compensation. In some mount or height-change paths, the CSS variable updates but the scroll position is not corrected, so the latest message can still be covered by the composer and the user cannot fully scroll past it.
Current evidence
packages/app/src/pages/session.tsx:1546 writes scroll container bottom padding with calc(var(--composer-dock-height, 0px) + 16px).
packages/app/src/pages/session.tsx:1898 observes the composer dock, writes --composer-dock-height, and performs bottom-scroll compensation when the dock height changes.
packages/app/src/pages/session.tsx:2028 also measures the prompt dock and writes the same CSS variable, but it does not perform bottom-scroll compensation. This creates two writers for the same layout state with different side effects.
Goal
Make the session page easier for humans and AI agents to maintain by turning session.tsx into a composition root instead of a mixed controller. The first implementation should fix the composer dock scrolling bug, then extract the directly related controller logic behind small, named hooks and components.
Scope
The first implementation PR may include both the scroll fix and the first controller extraction, but commits must stay review-sized. Behavior fixes, tests, refactors, visual spacing adjustments, and docs updates should be separate commits.
The first PR should focus on composer dock scrolling, scroll-dock ownership extraction, and final-turn spacing. Broader extractions such as history windowing, review state, follow-up state, and revert/restore are follow-up candidates under this issue, not required in the first PR.
The main target is packages/app/src/pages/session.tsx and closely related session-page modules. SessionSidePanel is intentionally out of scope for implementation in this issue. Right-panel product redesign is tracked separately in #154, and terminal-specific right-panel polish is tracked in #65.
Ownership rules
session.tsx should become the route-level composition root. It may read app context and wire controllers together, but the target architecture is that it should not directly own DOM measurement, timer-based loading retries, SDK mutations, VCS refresh state, scroll math, or composer dock layout state.
Shared layout state must have a single owner. In particular, composer dock height, --composer-dock-height, scroll container bottom padding, and bottom-scroll compensation should be owned by one controller.
New hooks and controllers should target roughly 200 lines where practical. This is a soft maintainability target, not a mechanical rule. Business cohesion wins over splitting purely to satisfy a line count.
Hooks should expose small public interfaces. They should not expose internal store setters unless the caller genuinely owns that state.
Acceptance criteria
When the timeline is already at the bottom, composer dock height changes keep the latest turn fully visible above the composer.
When the user has intentionally scrolled upward, composer dock height changes do not force the timeline back to the bottom.
The timeline can scroll fully past the composer dock, with no latest-message text hidden behind the composer.
After the correctness fix, the excessive final-turn gap is reduced in a separate visual commit and verified by screenshot, Browser Use, or Playwright smoke.
session.tsx no longer directly owns composer dock measurement and scroll compensation.
The first extracted controller has a clear name and narrow public API.
Unit tests cover the scroll-dock invariants. A browser or Playwright smoke check verifies the visible composer overlap case.
Non-goals
Do not rewrite the session UI.
Do not change the backend session protocol.
Do not redesign the right panel in this issue. See #154 and #65.
Do not move unrelated SessionSidePanel complexity into this PR.
Do not enforce a hard 200-line rule across all files.
Problem
packages/app/src/pages/session.tsxhas grown into a 2,145-line session-page controller. It now owns route state, visible-session state, message history, timeline scrolling, composer dock layout, follow-up queueing, revert/restore, review mode, VCS diff loading, artifact files, desktop context sync, keyboard focus, mobile tabs, and right-panel wiring.This makes local fixes risky because related state is split across distant blocks. The current composer dock scroll bug is the motivating defect: the timeline scroll container padding is written in one place, composer dock height is measured in two places, and only one path performs bottom-scroll compensation. In some mount or height-change paths, the CSS variable updates but the scroll position is not corrected, so the latest message can still be covered by the composer and the user cannot fully scroll past it.
Current evidence
packages/app/src/pages/session.tsx:1546writes scroll container bottom padding withcalc(var(--composer-dock-height, 0px) + 16px).packages/app/src/pages/session.tsx:1898observes the composer dock, writes--composer-dock-height, and performs bottom-scroll compensation when the dock height changes.packages/app/src/pages/session.tsx:2028also measures the prompt dock and writes the same CSS variable, but it does not perform bottom-scroll compensation. This creates two writers for the same layout state with different side effects.Goal
Make the session page easier for humans and AI agents to maintain by turning
session.tsxinto a composition root instead of a mixed controller. The first implementation should fix the composer dock scrolling bug, then extract the directly related controller logic behind small, named hooks and components.Scope
The first implementation PR may include both the scroll fix and the first controller extraction, but commits must stay review-sized. Behavior fixes, tests, refactors, visual spacing adjustments, and docs updates should be separate commits.
The first PR should focus on composer dock scrolling, scroll-dock ownership extraction, and final-turn spacing. Broader extractions such as history windowing, review state, follow-up state, and revert/restore are follow-up candidates under this issue, not required in the first PR.
The main target is
packages/app/src/pages/session.tsxand closely related session-page modules.SessionSidePanelis intentionally out of scope for implementation in this issue. Right-panel product redesign is tracked separately in #154, and terminal-specific right-panel polish is tracked in #65.Ownership rules
session.tsxshould become the route-level composition root. It may read app context and wire controllers together, but the target architecture is that it should not directly own DOM measurement, timer-based loading retries, SDK mutations, VCS refresh state, scroll math, or composer dock layout state.Shared layout state must have a single owner. In particular, composer dock height,
--composer-dock-height, scroll container bottom padding, and bottom-scroll compensation should be owned by one controller.New hooks and controllers should target roughly 200 lines where practical. This is a soft maintainability target, not a mechanical rule. Business cohesion wins over splitting purely to satisfy a line count.
Hooks should expose small public interfaces. They should not expose internal store setters unless the caller genuinely owns that state.
Acceptance criteria
When the timeline is already at the bottom, composer dock height changes keep the latest turn fully visible above the composer.
When the user has intentionally scrolled upward, composer dock height changes do not force the timeline back to the bottom.
The timeline can scroll fully past the composer dock, with no latest-message text hidden behind the composer.
After the correctness fix, the excessive final-turn gap is reduced in a separate visual commit and verified by screenshot, Browser Use, or Playwright smoke.
session.tsxno longer directly owns composer dock measurement and scroll compensation.The first extracted controller has a clear name and narrow public API.
Unit tests cover the scroll-dock invariants. A browser or Playwright smoke check verifies the visible composer overlap case.
Non-goals
Do not rewrite the session UI.
Do not change the backend session protocol.
Do not redesign the right panel in this issue. See #154 and #65.
Do not move unrelated
SessionSidePanelcomplexity into this PR.Do not enforce a hard 200-line rule across all files.