Skip to content

fix: keep long sessions pinned after submit#498

Merged
Astro-Han merged 3 commits into
devfrom
codex/fix-i496-scroll-jump
May 7, 2026
Merged

fix: keep long sessions pinned after submit#498
Astro-Han merged 3 commits into
devfrom
codex/fix-i496-scroll-jump

Conversation

@Astro-Han

@Astro-Han Astro-Han commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary

Keep long session timelines pinned to the latest turn after submit by adding a session-local bottom-follow lock around the submit/resume path. The lock is now scoped to the triggering session key and is cancelled by user navigation paths that intentionally leave the latest turn.

Why

Closes #496. In long sessions, the viewport could reset near the top shortly after a user submitted from the bottom. The issue evidence showed this happened within the same session, without a remount, history load, or scroll-height change, so the fix belongs in the session timeline scroll coordination layer.

Related Issue

Closes #496

Human Review Status

Pending. A human should make the final merge decision after reviewing the final diff and verification evidence.

Review Focus

Please focus on the bottom-follow lock lifetime, session-key scoping, real scroll gesture cancellation, and whether keyboard/hash/message navigation correctly cancels the lock without weakening the #496 reset recovery.

Risk Notes

Main risk is over-following if a user intentionally leaves the latest turn immediately after submit. The PR cancels the lock on real timeline scroll gestures, keyboard scroll keys, message navigation, and hash navigation, and refuses owner-mismatched restores.

How To Verify

Focused unit: 41 passed via bun test --preload ./happydom.ts ./src/pages/session/use-session-scroll-dock.test.ts ./src/pages/session/use-session-hash-scroll.test.ts ./src/pages/session/use-session-history-window.test.ts ./src/pages/session/session-auto-scroll.test.ts ./src/context/renderer-diagnostics.test.ts
E2E: 1 passed via bun --cwd packages/app test:e2e -- e2e/session/session-renderer-diagnostics.spec.ts
Typecheck: ok via bun --cwd packages/app typecheck
Electron smoke: bun run dev:desktop opened the PawWork dev app to the new-session screen without startup errors
Diff check: git diff --check passed

Screenshots or Recordings

No screenshot attached. Electron smoke verified the dev desktop app opens; the long-session reset behavior is covered by the deterministic E2E that forces the timeline viewport to top after submit.

Checklist

  • Human review status is stated above as pending, approved, or not required
  • I linked the related issue, or stated why there is no issue
  • This PR has type, primary area, and priority labels, or I requested maintainer labeling
  • I described the review focus and any meaningful risks
  • I listed the relevant verification steps and the key result for each
  • I did not introduce unrelated refactors, dependencies, generated files, or file changes beyond the stated scope
  • I manually checked visible UI or copy changes when needed, with screenshots or recordings
  • I considered macOS and Windows impact for platform, packaging, updater, signing, paths, shell, or permissions changes
  • I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, generated content, or local file changes when relevant
  • I reviewed the final diff for unrelated changes and suspicious dependency changes
  • I am targeting dev, and my PR title and commit messages use Conventional Commits in English

Summary by CodeRabbit

  • Tests

    • Expanded test coverage for session scroll behavior, timeline interaction, and scroll restoration scenarios.
  • Refactor

    • Improved scroll position management with enhanced locking and restoration mechanisms.
    • Enhanced message navigation and scroll gesture handling within session interactions.
    • Updated scroll behavior wiring to better coordinate timeline and session-level interactions.

@Astro-Han Astro-Han added bug Something isn't working P1 High priority app Application behavior and product flows labels May 7, 2026
@coderabbitai

coderabbitai Bot commented May 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Rate limit exceeded

@Astro-Han has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 48 minutes and 14 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 56527f63-1aaf-4342-8b08-a29a2f9b1970

📥 Commits

Reviewing files that changed from the base of the PR and between d54b662 and 494bba3.

📒 Files selected for processing (1)
  • packages/app/src/pages/session/use-session-scroll-dock.ts
📝 Walkthrough

Walkthrough

This PR introduces a bottom-follow lock mechanism to prevent session timeline scroll jumps during content updates. The scroll dock gains lock-state management, timeline interaction exports new helpers that coordinate with the lock, hash-scroll callbacks wire lock cancellation, and session components route through the new lock-aware system. Tests verify lock restoration across reset scenarios and E2E validation confirms scroll-anchor preservation.

Changes

Session Scroll-Jump Fix via Bottom-Follow Lock

Layer / File(s) Summary
Scroll Dock Lock API
packages/app/src/pages/session/use-session-scroll-dock.ts
Exported API expands with armBottomFollowLock(), cancelBottomFollowLock(), restoreBottomIfLocked(), and bottomFollowLocked() predicate. resumeScroll() signature changes to accept optional owner?: string. Hash-scroll input type gains optional onMessageNavigation?.() and onMessageHashCleared?.() callbacks.
Bottom-Follow Lock Implementation
packages/app/src/pages/session/use-session-scroll-dock.ts
Introduces timer-based lock state with owner tracking. followBottom() clears active/hash messages and forces scroll-to-bottom. scheduleScrollState conditionally calls followBottom() when lock is active and content overflows. resumeScroll(owner?) arms the lock and restores bottom instead of immediately forcing scroll. setScrollRef and setContentRef call restoreBottomIfLocked() to integrate lock restoration into lifecycle. Cleanup clears pending timeout.
Timeline Lock-Aware Coordination
packages/app/src/pages/session/use-session-timeline-interaction.ts
Imports Solid's createEffect and on. Introduces lockOwner() signal and derives userScrolledForHistory() from bottomFollowLocked(). Updates history window config to use lock-aware scroll state. Adds createEffect to restore bottom-follow when timeline dependencies change while scroll dock is locked to current owner.
Hash-Scroll Navigation Wiring
packages/app/src/pages/session/use-session-hash-scroll.ts, packages/app/src/pages/session/use-session-timeline-interaction.ts
Hash-scroll input type adds optional onMessageNavigation?.() and onMessageHashCleared?.() callbacks. scrollToMessage() and applyHash() invoke onMessageNavigation?.() at navigation points. Timeline interaction wires onMessageNavigation: scrollDock.cancelBottomFollowLock into hash-scroll setup.
Timeline Interaction Helpers
packages/app/src/pages/session/use-session-timeline-interaction.ts
Exports markScrollGesture(target?) and navigateMessageByOffset(offset) helpers that cancel the bottom-follow lock and delegate behavior to activeMessage.
Session Component Routing
packages/app/src/pages/session.tsx
useSessionKeyboardFocus receives markScrollGesture: timelineInteraction.markScrollGesture. useSessionCommands receives navigateMessageByOffset: timelineInteraction.navigateMessageByOffset. SessionMainView prop markScrollGesture switches from activeMessage.markScrollGesture to timelineInteraction.markScrollGesture.
Tests & E2E Validation
packages/app/src/pages/session/use-session-scroll-dock.test.ts, packages/app/src/pages/session/use-session-hash-scroll.test.ts, packages/app/e2e/session/session-renderer-diagnostics.spec.ts
Adds four unit tests for lock restoration: after submit-time reset, before scroll-state sampling, after lock cancellation, and session-scoped behavior. Adds integration test verifying onMessageNavigation callback wiring and timeline interaction exports. E2E test helper resetTimelineToTop() added; test flow verifies scroll anchor preservation and distance-from-bottom bounds after reset and new message submission.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • Astro-Han/pawwork#496: Directly addresses the reported session timeline scroll-jump bug where the viewport jumps near the top after sending a message; the bottom-follow lock mechanism preserves scroll position through DOM updates and content changes to fix this behavior.

Possibly related PRs

  • Astro-Han/pawwork#354: Both PRs modify the session scroll/timeline subsystem (scroll-dock, timeline interaction, and session.tsx wiring—including markScrollGesture/navigateMessageByOffset and bottom-follow behavior), so they are directly related at the code level.
  • Astro-Han/pawwork#286: Both PRs modify session scroll/hash handling (notably changes to use-session-hash-scroll and timeline interaction wiring to prevent stale-hash reapplication and to control scroll behavior), so they are related.
  • Astro-Han/pawwork#402: Both PRs are related — they both change session timeline/hash-scroll wiring and timeline interaction helpers (notably edits to use-session-hash-scroll and use-session-timeline-interaction), indicating direct overlap in the same functions and callbacks.

Poem

🐰 A rabbit hops through scrolling heights,
Where locks now guard the timeline's flight—
No jump to top when messages send,
The bottom stays, till bitter end!
With coordination, smooth and bright, 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a bottom-follow lock to keep long sessions pinned after submit.
Description check ✅ Passed The description follows the template with all required sections completed: summary, why, related issue, review focus, risk notes, verification steps, and full checklist.
Linked Issues check ✅ Passed All coding requirements from issue #496 are met: bottom-follow lock prevents timeline jumps after submit, lock is session-scoped and cancellable, E2E test covers the long-session case, and deterministic coverage for submit path is provided.
Out of Scope Changes check ✅ Passed All changes directly address the #496 issue objective of preventing long-session timeline jumps after submit; no unrelated refactors or file changes detected beyond the stated scope.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/fix-i496-scroll-jump

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a 'bottom follow lock' mechanism to the session scroll dock to ensure consistent scroll behavior during message submission and history loading, accompanied by new unit and E2E tests. Feedback focuses on improving code clarity by simplifying the timeout callback in armBottomFollowLock and reducing duplication by extracting shared state-clearing and scrolling logic into a helper function.

Comment thread packages/app/src/pages/session/use-session-scroll-dock.ts Outdated
Comment thread packages/app/src/pages/session/use-session-scroll-dock.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/app/src/pages/session/use-session-scroll-dock.ts (2)

109-127: 💤 Low value

Extract the 3s lock window into a named constant.

The lock lifetime is the central tuning knob for this feature (it bounds how long a browser-induced reset can be repaired without user input), but it's a bare 3_000 literal in the middle of armBottomFollowLock. Lifting it to a named constant at module scope (e.g. BOTTOM_FOLLOW_LOCK_MS) makes the intent obvious, gives tests a single place to override timing if needed, and documents the decision.

♻️ Proposed change
+const BOTTOM_FOLLOW_LOCK_MS = 3_000
+
 ...
   const armBottomFollowLock = (owner?: string) => {
     if (bottomFollowLockTimer !== undefined) clearTimeout(bottomFollowLockTimer)
     bottomFollowLockOwner = owner
     setBottomFollowLocked(true)
-    bottomFollowLockTimer = setTimeout(cancelBottomFollowLock, 3_000)
+    bottomFollowLockTimer = setTimeout(cancelBottomFollowLock, BOTTOM_FOLLOW_LOCK_MS)
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app/src/pages/session/use-session-scroll-dock.ts` around lines 109 -
127, Extract the hard-coded 3_000 timeout into a module-level named constant
(e.g., BOTTOM_FOLLOW_LOCK_MS) and use it inside armBottomFollowLock when calling
setTimeout; update any related references (bottomFollowLockTimer,
armBottomFollowLock, cancelBottomFollowLock, bottomFollowLocked,
bottomFollowLockOwner) to rely on that constant so the lock lifetime is
configurable and clearly documented for tests and future tuning.

175-183: 💤 Low value

Document the cancel-on-mismatch semantics of restoreBottomIfLocked.

When a caller passes an owner that does not match the active lock, this helper not only returns false but also cancels the lock outright. This is exercised by the "lock belongs to another session" unit test, but the contract is non-obvious from the function shape — most readers will assume a query helper is side-effect-free. A short comment explaining the intent (a different session interacting with the dock invalidates a stale lock) will save future maintainers a code-archeology trip.

📝 Proposed comment
+  // Querying with a non-matching owner cancels the lock: any interaction
+  // identifying itself as a different session means the previously-armed
+  // lock is stale and must not survive into the new session's scroll loop.
   const restoreBottomIfLocked = (owner?: string) => {
     if (!bottomFollowLockedFor(owner)) {
       if (bottomFollowLocked() && owner !== undefined) cancelBottomFollowLock()
       return false
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/app/src/pages/session/use-session-scroll-dock.ts` around lines 175 -
183, Add a brief doc comment above restoreBottomIfLocked explaining its
cancel-on-mismatch semantics: when an owner argument is provided that does not
match the current lock (checked via bottomFollowLockedFor), the function not
only returns false but also clears the active lock by calling
cancelBottomFollowLock; this behavior intentionally invalidates stale locks from
other sessions. Mention the related functions bottomFollowLocked,
cancelBottomFollowLock, followBottom, and scheduleScrollState (and scroller) so
readers understand the side effect and when followBottom/scheduleScrollState
will be invoked.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/app/src/pages/session/use-session-scroll-dock.ts`:
- Around line 109-127: Extract the hard-coded 3_000 timeout into a module-level
named constant (e.g., BOTTOM_FOLLOW_LOCK_MS) and use it inside
armBottomFollowLock when calling setTimeout; update any related references
(bottomFollowLockTimer, armBottomFollowLock, cancelBottomFollowLock,
bottomFollowLocked, bottomFollowLockOwner) to rely on that constant so the lock
lifetime is configurable and clearly documented for tests and future tuning.
- Around line 175-183: Add a brief doc comment above restoreBottomIfLocked
explaining its cancel-on-mismatch semantics: when an owner argument is provided
that does not match the current lock (checked via bottomFollowLockedFor), the
function not only returns false but also clears the active lock by calling
cancelBottomFollowLock; this behavior intentionally invalidates stale locks from
other sessions. Mention the related functions bottomFollowLocked,
cancelBottomFollowLock, followBottom, and scheduleScrollState (and scroller) so
readers understand the side effect and when followBottom/scheduleScrollState
will be invoked.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2f089686-1308-48a7-a933-a9b11b193e3f

📥 Commits

Reviewing files that changed from the base of the PR and between 95826de and d54b662.

📒 Files selected for processing (7)
  • packages/app/e2e/session/session-renderer-diagnostics.spec.ts
  • packages/app/src/pages/session.tsx
  • packages/app/src/pages/session/use-session-hash-scroll.test.ts
  • packages/app/src/pages/session/use-session-hash-scroll.ts
  • packages/app/src/pages/session/use-session-scroll-dock.test.ts
  • packages/app/src/pages/session/use-session-scroll-dock.ts
  • packages/app/src/pages/session/use-session-timeline-interaction.ts

@Astro-Han Astro-Han merged commit 4059947 into dev May 7, 2026
20 checks passed
@Astro-Han Astro-Han deleted the codex/fix-i496-scroll-jump branch May 7, 2026 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app Application behavior and product flows bug Something isn't working P1 High priority

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Long session jumps near top after sending a message

1 participant