Files

182 lines
9.0 KiB
Markdown

---
created: 2026-09-02
status: done
requirements:
- REQ-UI-TRANSCRIPT-AUTO-SCROLL-001
system_design:
- ../../specs/ui/system-design/transcript-auto-scroll.md
legacy_specs: []
---
# Implementation Plan: Restore Session Transcript Scroll on Activation
## Overview
Make transcript placement wait for a persistent desktop session panel to be
visible and measurable. Preserve bottom-follow intent through inactive tabs,
while retaining reader-owned positions when auto-scroll is disabled or the
reader has moved away from the newest message.
## Scope
### In scope
- Defer one-time transcript placement until an inactive Dockview panel becomes
visible.
- Reconcile bottom-follow intent after `SessionPanelContent` restores its saved
absolute offset.
- Suppress bottom writes and near-bottom geometry updates while a transcript is
hidden.
- Preserve unread-divider, explicit navigation, layout-restore, pagination,
disabled-position, and manual-reader ownership.
- Add focused hook regressions and desktop browser coverage for refresh, session
switching, hidden message delivery, and disabled-position preservation.
- Run the existing mobile auto-scroll suite against the shared scroll logic.
### Out of scope
- Changing session tabs, transcript controls, labels, layout, or persistence.
- Replacing Dockview persistent portals or the generic panel scroll restorer.
- Changing unread-cursor semantics, message pagination, or smooth-navigation
behavior.
- Adding a mobile persistent-panel lifecycle that the product does not use.
## Confirmed root cause
Dockview panel content is rendered through persistent portals. After a refresh,
an inactive session transcript can mount with its full message list while its
scroll container has zero geometry. Both `useInitialScrollPosition` and
`useScrollToDividerOrBottom` mark their one-time work complete in that state, so
activating the tab does not retry placement.
Live updates have a second failure path. `useAutoScroll` writes a bottom offset
while the inactive container is not measurable. When the tab becomes visible,
`SessionPanelContent` restores the last visible absolute `scrollTop`, which no
longer represents the bottom after hidden content growth.
The reproduced inactive transcript had 88 rows and a 3,712 px bottom gap after
refresh and activation. A transcript that received hidden agent output reopened
603 px above the bottom even though auto-scroll remained enabled.
## Technical approach
### Visibility-aware placement
- Pass the existing `isVisible` signal through the native initial-placement,
divider-placement, and auto-scroll hooks.
- Do not consume an initial-placement latch while `isVisible` is false.
- When an inactive panel becomes visible, defer placement by two animation
frames so the existing one-frame `SessionPanelContent` restore finishes
first.
- Keep `SessionPanelContent` generic, but apply its queued absolute-offset write
only when the panel's `scrollTop` is unchanged since scheduling. A newer
transcript or user scroll therefore remains the active owner.
- Re-check the live visibility and ownership guards before the deferred write,
and cancel the scheduled work if the panel becomes inactive again.
### Bottom-follow intent
- Keep the existing near-bottom ref as the transient bottom-follow intent.
- Do not write bottom offsets or derive near-bottom state from hidden geometry.
- On activation, place an enabled bottom-following transcript at the native
maximum offset through `scrollNativeToBottom`.
- Leave the restored absolute offset untouched when auto-scroll is disabled or
the reader moved away from the bottom before hiding.
### Competing scroll owners
- Keep pending Dockview layout restoration and explicit message navigation
ahead of automatic placement.
- Keep the visit-scoped unread divider ahead of bottom placement and mark that
placement as away from the bottom.
- Run the existing pagination recheck after visibility restoration without
allowing it to consume or override the placement decision.
## Tests
| Acceptance criterion | Test evidence |
| --- | --- |
| `AC-UI-TRANSCRIPT-AUTO-SCROLL-001.8` | A native-scroll hook regression mounts an overflowing transcript hidden, proves no initial latch/write is consumed, then activates it after a simulated panel restore and requires bottom placement. A desktop Playwright scenario refreshes a task with two long session histories, activates the initially inactive tab, and requires a near-zero bottom gap. |
| `AC-UI-TRANSCRIPT-AUTO-SCROLL-001.9` | A hook regression appends while hidden and requires one guarded catch-up on activation. The desktop browser scenario seeds a live message into an inactive session, reactivates it, and requires the marker and transcript bottom to be visible. |
| `AC-UI-TRANSCRIPT-AUTO-SCROLL-001.10` | Hook tests cover disabled and manually-away states. A desktop browser scenario disables auto-scroll, switches tabs, and requires the prior absolute position after returning. |
## E2E tests
- Desktop: extend `apps/web/e2e/tests/chat/auto-scroll-toggle.spec.ts` with a
deterministic two-session fixture. Use the existing E2E session/message
harness so both histories overflow before refresh and an inactive session can
receive a live `message.added` event without a timing delay.
- Mobile: no new scenario models the desktop-only persistent hidden portal.
Run the complete `mobile-auto-scroll-toggle.spec.ts` suite to prove the shared
native coordinator retains visible initial placement, enabled following,
disabled freezing, and touch control behavior.
## Mobile design contract
- **Desktop outcome:** switching to an enabled, bottom-following session tab
shows its newest message after refresh and hidden updates.
- **Mobile entry point:** the existing selected Chat panel under
`MobileSessionsPicker`.
- **Nearest shipped exemplar:** `SessionMobileLayout` renders one selected
`TaskChatPanel`; the existing mobile auto-scroll Playwright suite exercises
that shared native transcript.
- **Hierarchy and presentation:** no controls or composition change. The
transcript remains the only vertical scroll owner.
- **Shared state and logic:** desktop and mobile keep the same auto-scroll
preference, near-bottom intent, and bottom helper. Only desktop supplies an
inactive `isVisible` lifecycle.
- **Mobile coverage decision:** the defect state is unavailable on mobile
because inactive session panels are not kept mounted. The existing
mobile-specific Playwright suite is the narrow parity proof for the shared
logic changed by this repair.
## Work orders
- [x] [Task 01: Reconcile transcript scroll on session activation](task-01-reconcile-transcript-scroll-on-activation.md) (done)
## Verification strategy
- TDD regressions were added for hidden activation, hidden updates, competing
scroll owners, bounded-frame cancellation, and the activation-scoped
unread-divider settling window before the implementation was completed.
- A focused `SessionPanelContent` component test controls `ResizeObserver` and
`requestAnimationFrame` to prove both guarded restore outcomes: unchanged
`scrollTop` is restored, while a newer scroll remains authoritative.
- Focused native transcript unit tests, TypeScript typecheck, and targeted
ESLint passed.
- The desktop multi-session scenario and the existing mobile auto-scroll suite
passed with retries disabled.
- Specification tests, full specification lint, and diff checks passed.
## Risks
- A visibility correction can race the generic panel restore and be overwritten
if it runs too early. The two-frame schedule and stale-offset guard bound this
race without making the generic panel component chat-aware.
- A deferred correction can override an explicit jump, unread divider, or
reader-owned position if ownership is not re-checked at execution time.
- Hidden message updates can incorrectly turn a manual reading position back
into bottom-follow intent if geometry is sampled while inactive.
- Animation-frame tests can pass in jsdom while missing Dockview's detached DOM;
the real two-session browser regression is required evidence.
## Decisions
- No ADR is required. The repair preserves the existing Dockview portal,
transcript scroll-owner, and persisted-setting boundaries documented by the
current UI system design.
- No public documentation change is required because the fix restores existing
user-facing behavior and adds no control, setting, or workflow.
## Results
Implemented visibility-aware transcript placement, hidden-update catch-up,
reader-position preservation, and the cooperative generic panel restore guard.
The fixup pass centralized the shared activation-transition lifecycle, retained
the last visible disabled-reader offset through hidden cleanup, and covered
immediate activation-frame cancellation. The divider settling deadline now
starts at first visibility and resets on each hidden-to-visible activation.
Focused unit tests, the direct shared-panel restore regression, typecheck,
targeted ESLint, desktop and mobile Playwright coverage, specification lint,
and diff checks all passed.