Implement scroll preservation contract #20
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
posta/chat#20
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What to build
Implement the scroll preservation contract defined in
CONTEXT.md § Scroll preservation contract.Add
decideScroll(state) → {scrollTo: number}as a pure function onwindow.__liveUpdates, wherestatecarries pre-swap and post-swap measurements:scrollTopBefore,scrollHeightBefore,clientHeightBeforescrollHeightAfter,clientHeightAftertrigger: one ofcold-load,live-update,load-older,resumeWire
htmx:beforeSwap(stash measurements) andhtmx:afterSwap(apply verdict) for#main. Write goja tests for each row of the contract table.The contract rows (from CONTEXT.md):
scrollTop, no jumpAcceptance criteria
decideScrollcovers every row of the contractscrollTop + clientHeight >= scrollHeight - 96exactly (per CONTEXT.md)Blocked by
Agent Brief
Category: enhancement
Summary: Implement the scroll preservation contract from
CONTEXT.md § Scroll preservation contract. Add adecideScrollpure function onwindow.__liveUpdatesand wire it into htmx's swap hooks for the thread main pane. Add goja test cases for every row of the contract.Current behavior:
When htmx swaps the thread's
<main>(hx-swap="outerHTML"), the scroll container is replaced wholesale and the previous scroll position is lost. No code captures or restores it. Users scrolled up reading history lose their place when a live update arrives; users at the bottom may or may not stay pinned. On cold load and OS-suspend resume the behavior is similarly arbitrary.Desired behavior:
The bridge honors the contract published in
CONTEXT.md § Scroll preservation contract:scrollTop, no jump"At bottom" means
scrollTop + clientHeight >= scrollHeight - 96. The tolerance constant is exactly 96, sourced fromCONTEXT.md; any change to it changesCONTEXT.mdfirst.Implementation shape:
A pure decision function
decideScroll(state)lives onwindow.__liveUpdates(the pattern established by the bridge refactor). It receives a plain-data state describing the swap:scrollTopBefore,scrollHeightBefore,clientHeightBeforescrollHeightAfter,clientHeightAftertrigger: one ofcold-load,live-update,load-older,resumeIt returns
{scrollTo: <number>}, where the number is thescrollTopvalue to apply.The DOM-touching shell listens for
htmx:beforeSwapon#main(capture pre-swap measurements), thenhtmx:afterSwapon#main(calldecideScroll, apply the verdict). Tab-resume triggers a swap manually if no swap was already in flight; the agent decides the cleanest mechanism that doesn't double-fire.Cold load is handled by a one-shot scroll on initial paint that calls
decideScrollwithtrigger: cold-loadand the relevant measurements.Test coverage (via the goja harness from the previous issue):
decideScroll. Specifically:scrollHeightAfter - clientHeightAfter(bottom)scrollHeightAfter - clientHeightAfter(new bottom)scrollTopBeforescrollTopBefore + (scrollHeightAfter - scrollHeightBefore)(viewport anchor)scrollHeightAfter - clientHeightAfter(bottom)Acceptance criteria:
decideScrollis callable from goja and covers every contract rowhtmx:beforeSwapcaptures pre-swap measurements for#mainswapshtmx:afterSwapapplies thedecideScrollverdict for#mainswapsOut of scope: