fix(web): recover restored transcript pagination (#3252)
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
---
|
||||
created: 2026-09-01
|
||||
status: implemented
|
||||
requirements:
|
||||
- REQ-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001
|
||||
system_design:
|
||||
- ../../specs/ui/system-design/task-prompt-transcript-visibility.md
|
||||
legacy_specs: []
|
||||
---
|
||||
|
||||
# Implementation plan: Restored transcript pagination recovery
|
||||
|
||||
## Overview
|
||||
|
||||
Repair older-history pagination when a persistent Dockview chat is initialized
|
||||
while inactive and later becomes visible at its restored top edge. The fix will
|
||||
turn the existing panel-visibility signal and hard-top input into guarded
|
||||
current-geometry checks, without changing backend cursors, eager-opening
|
||||
history, or explicit failure recovery.
|
||||
|
||||
## Scope
|
||||
|
||||
### In scope
|
||||
|
||||
- Pass real chat-host visibility into the native transcript lifecycle.
|
||||
- Re-evaluate the oldest-page sentinel once after a hidden Dockview transcript
|
||||
becomes visible and its saved scroll offset is restored.
|
||||
- Accept upward input at `scrollTop === 0` as fresh pagination intent even when
|
||||
the browser emits no `scroll` event.
|
||||
- Reuse current sentinel geometry, serialization, session-epoch, and recovery
|
||||
guards for both paths.
|
||||
- Add focused unit regressions and a desktop restored-secondary-tab browser
|
||||
regression.
|
||||
- Retain mobile pagination, anchor, and recovery behavior through the shared
|
||||
native transcript path.
|
||||
|
||||
### Out of scope
|
||||
|
||||
- Backend message queries, pagination cursors, ordering, persistence, or page
|
||||
sizes.
|
||||
- Eagerly loading older history when a task opens away from the oldest loaded
|
||||
edge.
|
||||
- Retrying rejected or zero-progress requests on panel activation.
|
||||
- Changes to Prompt History pagination, Dockview layout persistence, or chat
|
||||
scroll ownership.
|
||||
- New user-facing copy or a new mobile composition.
|
||||
|
||||
## Confirmed root cause
|
||||
|
||||
The affected secondary session is restored as an inactive Dockview panel. Its
|
||||
message request succeeds with 100 newest rows, `has_more: true`, and a valid
|
||||
oldest cursor. The backend also returns older rows when that cursor is supplied.
|
||||
|
||||
The inactive transcript mounts behind a zero-size panel. When it becomes
|
||||
visible already at `scrollTop = 0`, its `IntersectionObserver` does not deliver
|
||||
a fresh eligible entry. The fallback in
|
||||
`useRetryPaginationOnUpwardScroll` requires
|
||||
`nextScrollTop < previousScrollTop`; at the hard top that comparison is
|
||||
`0 < 0`, so another upward gesture cannot invoke the sentinel retry. The
|
||||
background newest-window fetch then repeats the same 100 rows and cursor while
|
||||
the older-page path is never called.
|
||||
|
||||
## Technical approach
|
||||
|
||||
### Visibility-aware sentinel recovery
|
||||
|
||||
- Extend the message-list contract with the existing `TaskChatPanel`
|
||||
visibility state and carry it into native scroll management.
|
||||
- On a hidden-to-visible transition, wait for the panel's existing saved-offset
|
||||
restoration to commit before issuing one lifecycle recheck.
|
||||
- Make the shared sentinel recheck current root/sentinel geometry instead of
|
||||
trusting an intersection sample captured while hidden.
|
||||
- Fire only when visible pagination remains, initial/refetch loading is clear,
|
||||
the sentinel is inside preload, no request is active, the session is current,
|
||||
and transcript recovery is not active.
|
||||
- Keep the first visible mount bounded: a lifecycle recheck does not paginate
|
||||
unless the restored viewport is already at the oldest loaded edge.
|
||||
|
||||
### Hard-top user intent
|
||||
|
||||
- Observe directional wheel, keyboard, touch, and scroll movement at the
|
||||
transcript scroll owner.
|
||||
- Treat an upward action at the hard top as fresh intent even when scroll
|
||||
position does not change.
|
||||
- Route the action through the same current-geometry sentinel recheck and
|
||||
programmatic-scroll guard.
|
||||
- Coalesce one physical gesture so it cannot start duplicate cursor requests.
|
||||
|
||||
### Diagnostics and failure behavior
|
||||
|
||||
- Retain the existing bounded pagination diagnostics so the cursor-based older
|
||||
request remains distinguishable from newest-window running backfill.
|
||||
- Preserve explicit retry for rejected or zero-progress requests; activation
|
||||
must not create an automatic retry loop.
|
||||
- Preserve anchor restoration and continuation stop reasons after a positive
|
||||
older page commits.
|
||||
|
||||
## Tests
|
||||
|
||||
| Acceptance criteria | Evidence |
|
||||
| --- | --- |
|
||||
| `AC-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001.5` to `.9` | Existing sentinel and native transcript suites plus new current-geometry and hard-top regressions |
|
||||
| `AC-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001.10` | Existing no-eager-open browser assertion remains green |
|
||||
| `AC-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001.11` | Existing mobile pagination suite remains green after the shared-path change |
|
||||
| `AC-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001.12` | Existing inactive-session reconciliation coverage remains green |
|
||||
| `AC-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001.14` | New hidden-to-visible unit regression and restored secondary-session desktop browser flow |
|
||||
|
||||
The red-first unit case mounts a transcript hidden with `hasMore: true`, makes
|
||||
it visible at restored `scrollTop = 0`, and expects exactly one older-page
|
||||
request after the restore frame. A second case dispatches upward input at hard
|
||||
top without changing `scrollTop` and expects the guarded retry path to run.
|
||||
|
||||
## E2E tests
|
||||
|
||||
- Desktop Chromium: extend
|
||||
`apps/web/e2e/tests/chat/message-pagination.spec.ts` with two seeded sessions
|
||||
in one restored Dockview group. Keep the target session inactive during
|
||||
hydration, suppress the fresh observer entry that can be missed during
|
||||
restoration, activate its tab, supply upward input at the hard top, and
|
||||
assert that the next request includes the previous oldest cursor and reveals
|
||||
older content.
|
||||
- Keep the watcher capable of distinguishing newest-window refreshes from
|
||||
`before=<cursor>` older-page requests. Assert that activation starts one
|
||||
cursor advance, not repeated newest-window backfill.
|
||||
- Mobile Chrome: rerun
|
||||
`apps/web/e2e/tests/chat/mobile-message-pagination.spec.ts` as regression
|
||||
evidence for shared sentinel, anchor, and recovery behavior. No new mobile
|
||||
layout is required because the defect depends on inactive Dockview tabs.
|
||||
|
||||
## Mobile design contract
|
||||
|
||||
- Desktop outcome: activating a restored secondary chat at its saved top edge
|
||||
resumes older-history pagination without scroll-away and scroll-back.
|
||||
- Mobile entry point: the existing full-height Chat tab.
|
||||
- Nearest exemplar: `apps/web/components/task/task-layout.tsx` and the existing
|
||||
mobile message-pagination specification.
|
||||
- Hierarchy and surface: the transcript remains the only vertical scroll
|
||||
owner; no sheet, drawer, or compact desktop tab is added.
|
||||
- Shared logic: visibility and hard-top intent use the native transcript's
|
||||
sentinel guards. Mobile supplies mounted visibility rather than Dockview
|
||||
activation.
|
||||
- Mobile proof: the existing mobile long-history, prompt boundary, recovery,
|
||||
and prepend-anchor cases remain green.
|
||||
|
||||
## Work orders
|
||||
|
||||
- [x] [Task 01: Recover pagination after transcript activation](task-01-recover-pagination-after-activation.md) (`done`)
|
||||
|
||||
## Verification results
|
||||
|
||||
- Focused Vitest: 87 tests passed across the shared sentinel, native transcript,
|
||||
and lazy-message pagination suites.
|
||||
- TypeScript typecheck, targeted ESLint, targeted Prettier, and the complete spec
|
||||
linter passed.
|
||||
- Desktop Chromium: all eight message-pagination cases passed, including the
|
||||
restored inactive-session hard-top regression.
|
||||
- Mobile Chrome: all eight mobile message-pagination cases passed, including
|
||||
the hard-top touch recovery regression.
|
||||
- A fresh desktop PR screenshot was captured after the restored secondary chat
|
||||
loaded its older marker.
|
||||
|
||||
## Risks
|
||||
|
||||
- Running the lifecycle check before `SessionPanelContent` restores scroll can
|
||||
read zero-size or transient geometry and either miss or spuriously load.
|
||||
- Treating every resize or active render as intent can duplicate requests or
|
||||
create an automatic loop.
|
||||
- Direct changes to the shared sentinel can regress Prompt History's
|
||||
bottom-pinned append behavior.
|
||||
- Wheel, keyboard, scrollbar, and touch signals need coalescing while retaining
|
||||
true hard-top intent.
|
||||
- The browser fixture must reproduce restored inactive tabs, not only switch
|
||||
between two already-active panels.
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
---
|
||||
id: "01-recover-pagination-after-activation"
|
||||
title: "Recover pagination after transcript activation"
|
||||
status: done
|
||||
wave: 1
|
||||
depends_on: []
|
||||
plan: "plan.md"
|
||||
requirements:
|
||||
- REQ-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001
|
||||
acceptance_criteria:
|
||||
- AC-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001.5
|
||||
- AC-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001.7
|
||||
- AC-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001.8
|
||||
- AC-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001.9
|
||||
- AC-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001.10
|
||||
- AC-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001.11
|
||||
- AC-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001.12
|
||||
- AC-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001.14
|
||||
system_design:
|
||||
- ../../specs/ui/system-design/task-prompt-transcript-visibility.md
|
||||
---
|
||||
|
||||
# Task 01: Recover pagination after transcript activation
|
||||
|
||||
## Summary
|
||||
|
||||
Make a restored, previously hidden transcript re-evaluate older-page
|
||||
eligibility after activation, and make an upward action at hard top recover
|
||||
even when the browser emits no scroll-position change.
|
||||
|
||||
## In scope
|
||||
|
||||
- Add failing native transcript and sentinel regressions before production
|
||||
changes.
|
||||
- Carry real host visibility from `TaskChatPanel` through `MessageList` to
|
||||
native scroll management.
|
||||
- Add a one-shot hidden-to-visible current-geometry recheck after saved scroll
|
||||
restoration.
|
||||
- Add directional hard-top input recovery without treating programmatic scroll
|
||||
restoration as user intent.
|
||||
- Preserve request serialization, session epochs, continuation, failure
|
||||
recovery, and prepend anchoring.
|
||||
- Add a restored secondary-session desktop browser regression and run the
|
||||
existing mobile pagination suite.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Backend cursor, message-list, WebSocket, or persistence changes.
|
||||
- Prompt History behavior changes.
|
||||
- Automatic retries after rejected or zero-progress requests.
|
||||
- New UI controls, localized copy, or mobile navigation.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- A transcript hydrated while hidden issues exactly one cursor-based older-page
|
||||
request after it becomes visible at its restored oldest loaded edge.
|
||||
- Upward input at `scrollTop = 0` can start the same guarded request without a
|
||||
scroll event, while repeated lifecycle renders and one physical gesture do
|
||||
not duplicate it.
|
||||
- Desktop restored-tab E2E reaches older content, and existing desktop/mobile
|
||||
pagination, prompt-`#1`, bounded-opening, recovery, and anchor cases remain
|
||||
green.
|
||||
|
||||
## Verification
|
||||
|
||||
Run from `apps/web`:
|
||||
|
||||
```bash
|
||||
pnpm exec vitest run \
|
||||
hooks/use-lazy-load-sentinel.test.ts \
|
||||
components/task/chat/message-list-native.test.tsx \
|
||||
hooks/use-lazy-load-messages.test.ts
|
||||
pnpm run typecheck
|
||||
pnpm exec eslint \
|
||||
hooks/use-lazy-load-sentinel.ts \
|
||||
hooks/use-lazy-load-sentinel.test.ts \
|
||||
components/task/task-chat-panel.tsx \
|
||||
components/task/chat/message-list-shared.tsx \
|
||||
components/task/chat/message-list-native.tsx \
|
||||
components/task/chat/message-list-native-scroll.ts \
|
||||
components/task/chat/message-list-native.test.tsx \
|
||||
e2e/tests/chat/message-pagination-helpers.ts \
|
||||
e2e/tests/chat/message-pagination.spec.ts
|
||||
pnpm exec prettier --check \
|
||||
hooks/use-lazy-load-sentinel.ts \
|
||||
hooks/use-lazy-load-sentinel.test.ts \
|
||||
components/task/task-chat-panel.tsx \
|
||||
components/task/chat/message-list-shared.tsx \
|
||||
components/task/chat/message-list-native.tsx \
|
||||
components/task/chat/message-list-native-scroll.ts \
|
||||
components/task/chat/message-list-native.test.tsx \
|
||||
e2e/tests/chat/message-pagination-helpers.ts \
|
||||
e2e/tests/chat/message-pagination.spec.ts
|
||||
pnpm e2e:run --host --project chromium -- \
|
||||
tests/chat/message-pagination.spec.ts --grep "restored inactive session"
|
||||
pnpm e2e:run --host --no-build --project mobile-chrome -- \
|
||||
tests/chat/mobile-message-pagination.spec.ts
|
||||
```
|
||||
|
||||
## Files likely touched
|
||||
|
||||
- `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/hooks/use-lazy-load-sentinel.ts`
|
||||
- `apps/web/hooks/use-lazy-load-sentinel.test.ts`
|
||||
- `apps/web/e2e/tests/chat/message-pagination-helpers.ts`
|
||||
- `apps/web/e2e/tests/chat/message-pagination.spec.ts`
|
||||
|
||||
## Dependencies
|
||||
|
||||
None. The existing `usePanelActive` signal, `SessionPanelContent` scroll restore,
|
||||
message cursor API, and pagination fixtures are inputs to this work.
|
||||
|
||||
## Risks
|
||||
|
||||
- A recheck scheduled too early can run before restored geometry stabilizes.
|
||||
- Relaxing the sentinel's disarmed state can bypass explicit failure recovery.
|
||||
- Shared hook changes can affect Prompt History's bottom sentinel.
|
||||
- Input listeners can double-fire when one gesture also creates a scroll event.
|
||||
|
||||
## Parallelism
|
||||
|
||||
`sequential`
|
||||
|
||||
## Inputs
|
||||
|
||||
- `REQ-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001` acceptance criteria `.5`,
|
||||
`.7` to `.14`.
|
||||
- Upward pagination, failure and recovery, observability, and responsive
|
||||
sections of the paired system design.
|
||||
- Existing panel visibility, scroll restoration, sentinel, desktop pagination,
|
||||
and mobile pagination implementations and tests.
|
||||
|
||||
## Results
|
||||
|
||||
- Added a guarded current-geometry recheck to the shared lazy-load sentinel.
|
||||
- Carried Dockview visibility into native transcript scroll management and
|
||||
rechecked after hidden-to-visible restoration settles.
|
||||
- Added hard-top wheel, keyboard, and touch intent recovery while preserving
|
||||
programmatic-scroll and explicit-recovery guards.
|
||||
- Added red-first unit regressions and a restored secondary-session Chromium
|
||||
regression that proves the older request includes a cursor.
|
||||
- Verified 87 focused unit tests, eight desktop pagination cases, eight mobile
|
||||
pagination cases, typecheck, targeted lint/formatting, and spec lint.
|
||||
@@ -50,6 +50,7 @@ start.
|
||||
- **AC-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001.11:** Desktop and mobile transcripts shall provide the same history boundary, continuous upward loading, recovery, and scroll-position behavior.
|
||||
- **AC-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001.12:** When a previously opened session is revisited after more than one message page was persisted while it was inactive, the transcript shall reconcile to a contiguous newest window and upward pagination shall reach every persisted user prompt without gaps or duplicate rows.
|
||||
- **AC-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001.13:** When an upward lazy-load operation sizes its batch, it shall target 20 newly loaded text parts. Tool calls and other activity rows shall not advance that target; the operation can stop earlier when prompt `#1` is loaded, persisted history is exhausted, a request makes no progress, or its bounded safety limit is reached.
|
||||
- **AC-UI-TASK-PROMPT-TRANSCRIPT-VISIBILITY-001.14:** When a previously hidden transcript becomes visible with its restored viewport at the oldest loaded edge and older history remains, the transcript shall resume older-history loading without requiring the user to move away from and return to that edge.
|
||||
|
||||
## Exclusions
|
||||
|
||||
|
||||
@@ -44,9 +44,16 @@ and `prompt_index` field.
|
||||
pagination available to explicit recovery consumers.
|
||||
- The native transcript consumes `useLazyLoadMessages`. Prompt History consumes
|
||||
its own user-message request and pagination state.
|
||||
- The native transcript owns upward intent, committed sentinel geometry,
|
||||
scroll restoration, and retry-control visibility.
|
||||
- `useLazyLoadSentinel` owns observer lifecycle and request serialization.
|
||||
- `TaskChatPanel` supplies the native transcript with the host's real visibility
|
||||
state. Dockview hosts use panel activation; mobile and other direct hosts use
|
||||
their existing mounted visibility.
|
||||
- The native transcript owns upward intent, committed sentinel geometry, scroll
|
||||
restoration, host-visibility recovery, and retry-control visibility.
|
||||
- `SessionPanelContent` restores the saved scroll offset after a hidden panel
|
||||
receives non-zero geometry. The native transcript waits for that restore to
|
||||
commit before it asks the sentinel to re-evaluate the viewport.
|
||||
- `useLazyLoadSentinel` owns observer lifecycle, current-geometry eligibility,
|
||||
request serialization, and guarded retries from lifecycle or user intent.
|
||||
- Transcript navigation first uses loaded messages. When a selected prompt is
|
||||
absent, it requests an around window and merges the result before scrolling.
|
||||
- `requestOlderMessages` keeps transcript request coordination and raw cursor
|
||||
@@ -136,6 +143,21 @@ The transcript starts a load cycle when the user reaches the oldest loaded
|
||||
edge. Before each request it captures a stable rendered-row anchor. After the
|
||||
page commits, scroll restoration keeps that anchor at the same visual position.
|
||||
|
||||
A hidden Dockview panel can keep its transcript mounted while its scroll root
|
||||
has zero geometry. When that panel becomes visible, the native transcript
|
||||
waits until `SessionPanelContent` has restored the saved scroll offset, then
|
||||
performs one current-geometry eligibility check. If the restored sentinel is
|
||||
inside the preload region and pagination is otherwise eligible, that check
|
||||
starts or resumes the normal load cycle. It does not use an intersection entry
|
||||
captured while the panel was hidden.
|
||||
|
||||
An upward wheel, touch, keyboard, or scrollbar action at the hard top can
|
||||
produce no `scroll` event because `scrollTop` cannot decrease below zero. The
|
||||
native transcript therefore treats directional input at that boundary as
|
||||
fresh pagination intent and checks current sentinel geometry. Input and panel
|
||||
activation use the same request guards and cannot bypass loading, visible-start,
|
||||
failure-recovery, or session-epoch stops.
|
||||
|
||||
Continuation uses committed sentinel geometry, not the identity of the oldest
|
||||
standalone row:
|
||||
|
||||
@@ -161,6 +183,12 @@ automatic cycle is disarmed and the explicit older-page retry control is
|
||||
visible. A successful retry clears recovery state and resumes the geometry
|
||||
rule. Session changes, prompt `#1`, and raw exhaustion also clear it.
|
||||
|
||||
Panel activation does not retry a request that already entered recovery state.
|
||||
The lifecycle check repairs a missed observer transition only; request failure
|
||||
and no-progress outcomes still require the explicit retry control. Repeated
|
||||
active renders and resize notifications do not create additional pagination
|
||||
intent.
|
||||
|
||||
Routine successful pagination does not render the retry control. The control
|
||||
keeps the existing localized label and has a coarse-pointer hit area of at
|
||||
least 44 pixels without increasing fine-pointer desktop density.
|
||||
@@ -182,6 +210,12 @@ prompt returns the phone to Chat and uses the around-window path when the row is
|
||||
not loaded. The recovery button uses the same placement on both surfaces with a
|
||||
coarse-pointer touch target.
|
||||
|
||||
Dockview activation recovery is desktop-specific because inactive desktop tabs
|
||||
remain mounted with zero geometry. The eligibility and hard-top gesture rules
|
||||
remain in the shared native transcript path. Mobile has no hidden Dockview tab,
|
||||
so it keeps mounted visibility and must retain the same upward pagination,
|
||||
anchor, and failure-recovery behavior.
|
||||
|
||||
## Around-window navigation
|
||||
|
||||
The chat panel sends `GET /api/v1/task-sessions/:id/messages?around=<message-id>&sort=desc&limit=N` after it confirms that the target belongs to the active session. The backend returns the target and newer rows in newest-first order. The chat panel merges the response into `messages.bySession`, waits for the target row to render, and then scrolls to it. The target token is session-scoped and is cleared only after a successful scroll or a confirmed missing target. A stale response cannot consume a newer selection.
|
||||
@@ -199,10 +233,10 @@ anchor behavior as desktop.
|
||||
- Session-state tests cover initialization through boot hydration, newest
|
||||
fetch, purge, and refetch without inferring exhaustion from defaults.
|
||||
- Sentinel tests cover committed in-region continuation, out-of-region stop,
|
||||
stale observer entries, no-progress recovery, successful retry, and session
|
||||
reset.
|
||||
- Native transcript tests cover recovery-control visibility and prepend anchor
|
||||
preservation.
|
||||
stale observer entries, one-shot lifecycle rechecks, hard-top intent,
|
||||
no-progress recovery, successful retry, and session reset.
|
||||
- Native transcript tests cover hidden-to-visible recovery, recovery-control
|
||||
visibility, and prepend anchor preservation.
|
||||
- Lazy-load hook tests cover mixed `message`, `content`, legacy, and tool rows,
|
||||
the 20-text-part target, every early stop, and unchanged raw/single-page
|
||||
consumers.
|
||||
@@ -215,3 +249,8 @@ anchor behavior as desktop.
|
||||
must still cross that page and reveal the targeted older text batch.
|
||||
- Existing prompt-`#1`, hidden pre-prompt, and inactive-session reconciliation
|
||||
scenarios remain covered.
|
||||
- A desktop browser test restores two session tabs with the target tab inactive,
|
||||
suppresses the fresh observer entry that can be missed during restoration,
|
||||
activates the target, supplies upward input at the hard top, and proves that
|
||||
the next request carries an older-page cursor instead of repeating only the
|
||||
newest 100-row window.
|
||||
|
||||
Reference in New Issue
Block a user