182 lines
9.0 KiB
Markdown
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.
|