fix(web): restore chat scroll on env-changing task switch (#3342)
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
---
|
||||
created: 2026-09-03
|
||||
status: complete
|
||||
requirements:
|
||||
- REQ-UI-TRANSCRIPT-AUTO-SCROLL-001
|
||||
system_design:
|
||||
- ../../specs/ui/system-design/transcript-auto-scroll.md
|
||||
legacy_specs: []
|
||||
---
|
||||
|
||||
# Implementation Plan: Restore Transcript Scroll Across Environment Switches
|
||||
|
||||
## Overview
|
||||
|
||||
Make an environment-changing task switch request fresh placement for the
|
||||
incoming session after its Dockview layout and latest message window settle.
|
||||
Keep the request session-scoped so enabled transcripts land at the newest
|
||||
message, disabled transcripts use their own saved position, and the older-
|
||||
history sentinel cannot paginate from transient top geometry.
|
||||
|
||||
## Scope
|
||||
|
||||
### In scope
|
||||
|
||||
- Arm a tokened incoming-session placement request only for cross-environment
|
||||
Dockview switches.
|
||||
- Keep the absolute `pendingChatScrollTop` restore reserved for same-transcript
|
||||
layout rebuilds.
|
||||
- Expose cached session-entry history refresh readiness to the transcript
|
||||
coordinator without changing its loading presentation.
|
||||
- Re-arm initial placement for a matching request and resolve it after layout
|
||||
and history settle.
|
||||
- Block automatic older-history intersection loading until placement completes.
|
||||
- Add focused store, hook, and browser regressions for enabled, disabled,
|
||||
wrong-session-offset, pagination, and same-environment behavior.
|
||||
- Run the existing mobile auto-scroll suite because mobile shares the native
|
||||
transcript coordinator.
|
||||
|
||||
### Out of scope
|
||||
|
||||
- Changing the auto-scroll control, persisted preference, saved-offset format,
|
||||
or message-pagination contract.
|
||||
- Replaying the outgoing task's absolute transcript offset.
|
||||
- Changing Dockview layout persistence, portal ownership, or fast/slow switch
|
||||
selection.
|
||||
- Changing normal live tailing, unread-divider placement, explicit transcript
|
||||
navigation, maximize, un-maximize, preset, or custom-layout restoration.
|
||||
- Adding a mobile Dockview environment-switch lifecycle.
|
||||
|
||||
## Confirmed root cause
|
||||
|
||||
`buildSwitchToSession` routes a task whose session has a different environment
|
||||
through `performLayoutSwitch` and `buildEnvSwitchAction`. That action toggles
|
||||
`isRestoringLayout` around `performEnvSwitch`, but intentionally does not call
|
||||
`preserveChatScrollDuringLayout`, because its absolute `scrollTop` belongs to
|
||||
the outgoing session.
|
||||
|
||||
The incoming panel remains logically visible, so `useActivationPending` does
|
||||
not observe a hidden-to-visible transition. `useInitialScrollPosition` can then
|
||||
consume its one-shot latch against cached or partial rows before the incoming
|
||||
`message.list` refresh completes. Later rows change the content height without
|
||||
another placement. The transient near-top geometry exposes the lazy-load
|
||||
sentinel and can start repeated `top-intersection` pagination.
|
||||
|
||||
The cached-history entry path in `useSessionMessages` starts a background
|
||||
`message.list` refresh without including that operation in the loading state
|
||||
currently available to the transcript. Layout state alone therefore cannot
|
||||
prove that the incoming history has settled.
|
||||
|
||||
## Technical approach
|
||||
|
||||
### Session-scoped environment-switch request
|
||||
|
||||
- Extend `DockviewStore` with a transient tokened placement request containing
|
||||
the incoming `sessionId`, plus a compare-and-clear completion action.
|
||||
- Arm it inside `buildEnvSwitchAction` after the same-environment early return
|
||||
and before the layout mutation. Do not write `pendingChatScrollTop`.
|
||||
- Preserve the request through both `performEnvSwitch` paths and default-layout
|
||||
fallback. A later switch replaces the token.
|
||||
|
||||
### History and placement readiness
|
||||
|
||||
- Track the cached-history background refresh in `useSessionMessages` and
|
||||
expose a dedicated readiness value through `useChatPanelState` to
|
||||
`NativeMessageList`. Do not use it to add a new loading indicator.
|
||||
- Extend the activation-pending primitive with an optional external token so a
|
||||
matching environment-switch request re-arms placement while `isVisible`
|
||||
remains true.
|
||||
- Keep the initial-placement latch pending while the layout is restoring or
|
||||
incoming history is refreshing. Once both are ready, run placement through
|
||||
`scheduleAfterPanelRestore` and re-check all scroll owners before writing.
|
||||
- Resolve the position with the existing `resolveNativeInitialScrollTop` logic,
|
||||
using only the incoming `sessionId` for the saved offset. Clear the request
|
||||
only when the token still matches.
|
||||
|
||||
### Pagination isolation
|
||||
|
||||
- Treat a matching, unresolved environment-switch placement as a hard block for
|
||||
`useLazyLoadSentinel`, alongside initial/refetch loading.
|
||||
- Release the block only after placement has completed or a higher-priority
|
||||
explicit scroll owner has consumed the request.
|
||||
|
||||
## Tests
|
||||
|
||||
| Acceptance criterion | Test evidence |
|
||||
| --- | --- |
|
||||
| `AC-UI-TRANSCRIPT-AUTO-SCROLL-001.11` | `dockview-env-switch-action.test.ts` proves only a cross-environment switch arms the incoming session request. `message-list-native.test.tsx` holds layout/history pending, settles with a taller incoming transcript, and requires enabled placement at the current bottom. The desktop Playwright flow switches between two tasks with distinct environments and requires the returned enabled transcript to settle at the bottom. |
|
||||
| `AC-UI-TRANSCRIPT-AUTO-SCROLL-001.12` | Native-scroll regressions save distinct offsets for outgoing and incoming sessions and require the disabled incoming session's value. The desktop flow disables the incoming transcript, records its position, visits the other task, and requires the incoming position after return. |
|
||||
| `AC-UI-TRANSCRIPT-AUTO-SCROLL-001.13` | The native-scroll harness requires the lazy-load sentinel to stay blocked throughout unresolved environment placement and verifies no load starts from transient top geometry. |
|
||||
| Existing criteria 8-10 | Same-environment store and hook cases remain unchanged. The existing inactive-session desktop scenario and mobile suite continue to pass. |
|
||||
|
||||
`dockview-scroll-preserve.test.ts` also proves the absolute layout-preservation
|
||||
helper does not arm, replace, or clear the environment-switch request.
|
||||
|
||||
## E2E tests
|
||||
|
||||
- Desktop: extend `apps/web/e2e/tests/chat/auto-scroll-toggle.spec.ts` with a
|
||||
two-task fixture whose sessions have different task environments and
|
||||
overflowing persisted histories. Cover enabled return-to-bottom and disabled
|
||||
incoming-position restoration through sidebar task selection.
|
||||
- Mobile: the defect path is desktop Dockview-only. Run the existing
|
||||
`mobile-auto-scroll-toggle.spec.ts` suite to prove shared initial placement,
|
||||
bottom following, disabled freezing, and touch controls remain intact.
|
||||
|
||||
## Mobile design contract
|
||||
|
||||
- **Desktop outcome:** returning to a task in another environment shows its own
|
||||
transcript at the current bottom or its own disabled saved position.
|
||||
- **Mobile entry point:** the existing selected Chat panel under
|
||||
`MobileSessionsPicker`.
|
||||
- **Nearest shipped exemplar:** `SessionMobileLayout` renders one selected
|
||||
`TaskChatPanel`; the existing mobile auto-scroll suite covers the shared
|
||||
coordinator.
|
||||
- **Hierarchy and presentation:** no controls, composition, or scroll ownership
|
||||
change. The transcript remains the single vertical scroll owner.
|
||||
- **Shared state and logic:** desktop and mobile share session auto-scroll state
|
||||
and native placement. The transient Dockview request is desktop-only.
|
||||
- **Mobile coverage decision:** no new mobile scenario can reproduce a Dockview
|
||||
environment rebuild. Existing mobile Playwright coverage is the parity proof
|
||||
for the shared code path.
|
||||
|
||||
## Work orders
|
||||
|
||||
- [x] [Task 01: Reconcile environment-switch transcript placement](task-01-reconcile-env-switch-transcript-placement.md)
|
||||
|
||||
## Verification results
|
||||
|
||||
- Cross-environment Dockview switches now arm a tokened request for the incoming
|
||||
session. The native transcript holds placement and pagination until layout
|
||||
restoration and the session-entry history refresh settle.
|
||||
- Enabled transcripts resolve at the latest message. Disabled transcripts use
|
||||
only their own saved offset. Compare-and-clear tokens protect later switches,
|
||||
and the same-environment path remains a no-op.
|
||||
- Focused Vitest passed in test and production environment modes (128 tests).
|
||||
- The complete frontend Vitest run passed (1,756 files, 15,076 tests, 4
|
||||
skipped). Typecheck, lint, Prettier, spec lint, and diff checks passed.
|
||||
- The desktop differing-environment Playwright regression passed (1 test). The
|
||||
existing mobile auto-scroll suite passed (5 tests).
|
||||
|
||||
## Risks
|
||||
|
||||
- React effect order can let initial placement run before a session-entry fetch
|
||||
marks itself pending. Arming the store request before the layout mutation and
|
||||
reading a dedicated refresh signal prevents that first-render race.
|
||||
- A stale animation frame can target a session after another task switch. The
|
||||
token and live session/owner checks must reject it.
|
||||
- Releasing the pagination block before the bottom or saved-offset write can
|
||||
recreate the `top-intersection` cascade.
|
||||
- Broadening the existing loading presentation for cached history would be an
|
||||
unrelated UI change; readiness stays internal to scroll coordination.
|
||||
|
||||
## Decisions
|
||||
|
||||
- No ADR is required. The repair preserves the current Dockview, transcript
|
||||
scroll-owner, per-session storage, and pagination boundaries.
|
||||
- No public documentation change is required because the repair restores
|
||||
existing behavior and adds no user-facing control or workflow.
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
---
|
||||
id: "01-reconcile-env-switch-transcript-placement"
|
||||
title: "Reconcile environment-switch transcript placement"
|
||||
status: done
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
requirements:
|
||||
- REQ-UI-TRANSCRIPT-AUTO-SCROLL-001
|
||||
acceptance_criteria:
|
||||
- AC-UI-TRANSCRIPT-AUTO-SCROLL-001.11
|
||||
- AC-UI-TRANSCRIPT-AUTO-SCROLL-001.12
|
||||
- AC-UI-TRANSCRIPT-AUTO-SCROLL-001.13
|
||||
system_design:
|
||||
- ../../specs/ui/system-design/transcript-auto-scroll.md
|
||||
---
|
||||
|
||||
# Task 01: Reconcile Environment-switch Transcript Placement
|
||||
|
||||
## Summary
|
||||
|
||||
Arm a session-scoped placement request for environment-changing Dockview
|
||||
switches, defer the incoming transcript until its layout and latest history are
|
||||
ready, and block automatic older-history pagination during that window. Prove
|
||||
that enabled and disabled sessions resolve from their own state while same-
|
||||
environment and ordinary layout behavior remain unchanged.
|
||||
|
||||
## Failing regressions first
|
||||
|
||||
1. Add a store action test that performs an `env-a` to `env-b` switch and
|
||||
requires a tokened request for the incoming session while
|
||||
`pendingChatScrollTop` remains null. Require the same-environment early
|
||||
return not to arm a request.
|
||||
2. Add a native-scroll hook case with outgoing and incoming sessions, distinct
|
||||
saved offsets, a matching environment-switch token, and a pending history
|
||||
refresh. Require no initial write or pagination eligibility until the
|
||||
incoming rows settle.
|
||||
3. Complete the refresh with a larger scroll height and require an enabled
|
||||
session to land at the current bottom.
|
||||
4. Repeat with auto-scroll disabled and require the incoming session's saved
|
||||
offset, never the outgoing session's offset.
|
||||
5. Add a cached-entry session-message test that holds the background
|
||||
`message.list` promise and requires the placement-readiness signal to remain
|
||||
pending until that exact refresh settles.
|
||||
6. Add a desktop Playwright case that switches between two overflowing tasks
|
||||
in different environments and fails on the current top reset.
|
||||
|
||||
## In scope
|
||||
|
||||
- Add and consume the tokened Dockview transcript-placement request.
|
||||
- Extend activation pending to support a visible external reactivation token.
|
||||
- Expose cached history-refresh readiness without changing loading copy or
|
||||
presentation.
|
||||
- Block the older-history sentinel while environment placement is unresolved.
|
||||
- Cover the env-switch store action, absolute scroll preservation isolation,
|
||||
initial placement, session-specific saved offset, pagination block, and
|
||||
same-environment behavior.
|
||||
- Add the desktop differing-environment browser regression and run the existing
|
||||
mobile auto-scroll suite.
|
||||
- Update plan and work-order results after implementation.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Reusing `preserveChatScrollDuringLayout` for the environment switch.
|
||||
- Changing saved-offset persistence, auto-scroll controls, pagination page
|
||||
sizes, or message fetching contracts.
|
||||
- Changing maximize, un-maximize, preset/custom layout, explicit navigation,
|
||||
unread divider, or live-tailing behavior.
|
||||
- New copy, layout, responsive composition, or mobile navigation.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- A cross-environment switch places an enabled incoming transcript at the
|
||||
bottom after its latest history settles, without applying the outgoing
|
||||
session's offset.
|
||||
- A disabled incoming transcript restores only its own saved offset.
|
||||
- Automatic top-intersection pagination remains blocked until placement is
|
||||
complete, and same-environment switching retains the current path.
|
||||
|
||||
## Verification
|
||||
|
||||
```bash
|
||||
cd apps && pnpm --filter @kandev/web test -- lib/state/dockview-env-switch-action.test.ts lib/state/dockview-scroll-preserve.test.ts hooks/domains/session/use-session-messages.test.ts components/task/chat/transcript-auto-scroll.test.ts components/task/chat/message-list-native.test.tsx
|
||||
cd apps && NODE_ENV=production pnpm --filter @kandev/web test -- lib/state/dockview-env-switch-action.test.ts lib/state/dockview-scroll-preserve.test.ts hooks/domains/session/use-session-messages.test.ts components/task/chat/transcript-auto-scroll.test.ts components/task/chat/message-list-native.test.tsx
|
||||
cd apps/web && pnpm run typecheck
|
||||
cd apps && pnpm --filter @kandev/web lint
|
||||
cd apps/web && pnpm e2e:run --host --project chromium tests/chat/auto-scroll-toggle.spec.ts -- --grep "environment-changing task switch" --retries=0
|
||||
cd apps/web && pnpm e2e:run --host --no-build --project mobile-chrome tests/chat/mobile-auto-scroll-toggle.spec.ts -- --retries=0
|
||||
python3 scripts/lint-spec-files.test.py
|
||||
python3 scripts/lint-spec-files.py --all
|
||||
git diff --check -- docs/specs/ui docs/plans/transcript-env-switch-scroll apps/web/components/task/chat apps/web/components/task/task-chat-panel.tsx apps/web/hooks/domains/session apps/web/lib/state apps/web/e2e/tests/chat
|
||||
```
|
||||
|
||||
Run `pnpm install --frozen-lockfile` from `apps/` before the first package
|
||||
command in a fresh worktree.
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `apps/web/lib/state/dockview-store.ts`
|
||||
- `apps/web/lib/state/dockview-env-switch-action.test.ts`
|
||||
- `apps/web/lib/state/dockview-scroll-preserve.test.ts`
|
||||
- `apps/web/hooks/domains/session/use-session-messages.ts`
|
||||
- `apps/web/hooks/domains/session/use-session-messages.test.ts`
|
||||
- `apps/web/components/task/chat/use-chat-panel-state.ts`
|
||||
- `apps/web/components/task/task-chat-panel.tsx`
|
||||
- `apps/web/components/task/chat/message-list-shared.tsx`
|
||||
- `apps/web/components/task/chat/message-list-native.tsx`
|
||||
- `apps/web/components/task/chat/message-list-native-scroll.ts`
|
||||
- `apps/web/components/task/chat/message-list-native.test.tsx`
|
||||
- `apps/web/components/task/chat/transcript-auto-scroll.ts`
|
||||
- `apps/web/components/task/chat/transcript-auto-scroll.test.ts`
|
||||
- `apps/web/e2e/tests/chat/auto-scroll-toggle.spec.ts`
|
||||
|
||||
## Dependencies
|
||||
|
||||
None.
|
||||
|
||||
## Risks
|
||||
|
||||
- A request that is not session- and token-checked can clear or reposition a
|
||||
later task switch.
|
||||
- Placement must wait for both layout and cached-history refresh readiness, not
|
||||
only one signal.
|
||||
- The sentinel block must be released after the final placement write, not when
|
||||
the request is first observed.
|
||||
|
||||
## Parallelism
|
||||
|
||||
`sequential`
|
||||
|
||||
## Inputs
|
||||
|
||||
- `REQ-UI-TRANSCRIPT-AUTO-SCROLL-001`, criteria 11 through 13.
|
||||
- `docs/specs/ui/system-design/transcript-auto-scroll.md`, especially
|
||||
Environment-change rebuild lifecycle.
|
||||
- Existing env-switch store-action and native-scroll hook harnesses.
|
||||
- Existing desktop and mobile transcript auto-scroll Playwright suites.
|
||||
|
||||
## Results
|
||||
|
||||
- Added a session-scoped, tokened Dockview placement request that is armed only
|
||||
by environment-changing switches and never captures the outgoing transcript's
|
||||
absolute offset.
|
||||
- Added cached-entry history-refresh readiness and delayed native placement
|
||||
until both Dockview and message history settle. Auto-scroll writes and the
|
||||
older-history sentinel stay inactive while the request owns placement.
|
||||
- Added unit regressions for enabled bottom placement, disabled incoming offset,
|
||||
stale-token completion, same-environment behavior, pagination blocking, and
|
||||
isolation from absolute layout restore.
|
||||
- Added a desktop Playwright regression using two confirmed distinct task
|
||||
environments. It covers enabled and disabled return trips through the task
|
||||
sidebar.
|
||||
- Verification passed: focused Vitest in test and production modes (128 tests),
|
||||
full Vitest (1,756 files and 15,076 tests, with 4 skipped), typecheck, lint,
|
||||
Prettier, spec lint, desktop Playwright (1 test), and mobile Playwright (5
|
||||
tests).
|
||||
Reference in New Issue
Block a user