Files

9.0 KiB

created, status, requirements, system_design, legacy_specs
created status requirements system_design legacy_specs
2026-09-02 done
REQ-UI-TRANSCRIPT-AUTO-SCROLL-001
../../specs/ui/system-design/transcript-auto-scroll.md

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

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.