Skip to content

fix(app): stabilize timeline scroll recovery#916

Closed
Astro-Han wants to merge 11 commits into
devfrom
pawwork/fix-911-scroll-stability
Closed

fix(app): stabilize timeline scroll recovery#916
Astro-Han wants to merge 11 commits into
devfrom
pawwork/fix-911-scroll-stability

Conversation

@Astro-Han

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

Copy link
Copy Markdown
Owner

Summary

  • Preserve reading-history viewport position with fine-grained data-timeline-anchor sampling and fallback recovery.
  • Keep latest-following protected through dock/content layout settling unless explicit user navigation takes over.
  • Add targeted regression coverage for timeline anchors, latest protection, dock preserve-strategy wiring, a focused Playwright E2E weak-wheel completion path, re-keyed anchor fallback, hidden restore anchors, shifted/offscreen restore anchors, closed trow summary anchors, same-message multi-trow fallback scoping, and controller snapshot isolation.

Why

Issue #911 exposed timeline jumps from two related paths: tool/trow expansion while reading history used message-level anchors that were too coarse, and answer-completion dock/content resize could let weak upward input interrupt latest-following. This PR wires both paths into the existing scroll controller / layout transaction model without a broad rewrite.

Related Issue

Closes #911

Human Review Status

Pending

Review Focus

Please review the reading-anchor fallback chain and the latest-protection boundary: weak upward wheel/touch is ignored only while latest protection is active, while strong upward gestures, keyboard upward navigation, scrollbar drag, and target-message navigation still yield to user intent.

Risk Notes

  • Scroll behavior changes are intentionally limited to session timeline recovery paths, but regressions could affect long-session reading position or bottom-follow behavior.
  • Browser E2E coverage locks the latest-protected weak-wheel answer-completion path. Anchor fallback unit coverage includes disappearing/re-keyed anchors, hidden/zero-size/closed-details body restore anchors, closed details summary trow anchors, same-message multi-trow fallback scoping, and shifted/offscreen-but-restorable anchors.
  • Sampling and restore now use separate semantic predicates: sampling requires current viewport intersection; restore only rejects unusable anchors (hidden, closed details body descendants, zero-size/collapsed), so layout-shifted anchors and visible closed-details summaries can still recover the viewport.
  • Tool-anchor fallback now prefers the selected tool's nearest session-turn-trow-block before falling back to the broader message-level search.
  • Controller state snapshots deep-clone nested reading anchor fields so external callers cannot mutate controller internals through state() snapshots.
  • Platform/packaging impact: not applicable; app timeline logic only.

How To Verify

Focused E2E RED check against pre-fix parent commit: failed as expected
Command: bun test:e2e -- e2e/session/session-scroll-position.spec.ts -g "keeps latest pinned when weak upward wheel lands during answer completion"
Baseline failure: expected latest_protected_weak_upward_ignored diagnostic event, received false

Focused E2E on this branch: passed
Command: bun test:e2e -- e2e/session/session-scroll-position.spec.ts -g "keeps latest pinned when weak upward wheel lands during answer completion"

Targeted timeline unit tests: 74 passed
Command: bun test --preload ./happydom.ts ./src/pages/session/session-timeline-scroll-anchors.test.ts ./src/pages/session/session-timeline-scroll-controller.test.ts ./src/pages/session/use-session-scroll-dock.test.ts ./src/pages/session/timeline-layout-transaction.test.ts

Review-check opencode unit repro: passed locally
Command: bun test ./test/session/run-state.test.ts -t "defers disposeAll across all loaded directories when any directory has an active run"

App typecheck: passed
Command: bun run typecheck

Diff whitespace check: passed
Command: git diff --check

Visual snap: passed; reviewed session-trow grid output
Command: bun run snap session-trow
Output: docs/design/preview/screenshots/session-trow.png

Screenshots or Recordings

Session trow snap was regenerated and reviewed locally:

  • docs/design/preview/screenshots/session-trow.png

Checklist

How to use this checklist:

  • Tick a box by replacing [ ] with [x]. Do not edit, add, or remove items.
  • The bot-applied label items can only be honestly ticked AFTER the PR is opened and the labeler / priority-triage bots have run — return to the PR description and tick them then.
  • Most items are required. The few that are conditional are explicitly marked (conditional); for those, leave unticked if they truly do not apply and explain why in Risk Notes. All other items must be ticked before requesting human review.
  • Type label — this PR carries exactly one of bug, enhancement, task, documentation. Type labels are author-added; the labeler bot does NOT assign them. Add the label in the GitHub UI, then tick this.
  • Routing labels — this PR carries at least one of app, ui, platform, harness, ci. The labeler bot assigns these on PR open based on changed paths. Confirm the bot's choice (or override if wrong), then tick this.
  • Priority label — this PR carries exactly one of P0, P1, P2, P3. The priority-triage bot suggests one on PR open. Confirm or override, then tick this.
  • Human Review Status above is set to Pending, Approved by @<reviewer>, or Not required: <reason> (default is Pending; "not required" is restricted to bot-authored low-risk PRs).
  • I linked the related issue, or stated in Summary why there is no issue.
  • I described the review focus and any meaningful risks.
  • I replaced the example block in How To Verify with the real verification steps and the key result for each.
  • I did not introduce unrelated refactors, dependencies, generated files, or file changes beyond the stated scope.
  • (conditional) I manually checked visible UI or copy changes when needed, with screenshots or recordings. Leave unticked only if no visible UI or copy changed.
  • (conditional) I considered macOS and Windows impact for platform, packaging, updater, signing, paths, shell, or permissions changes. Leave unticked only if no platform/packaging surface was touched.
  • (conditional) I called out docs, release notes, dependencies, permissions, credentials, deletion behavior, generated content, or local file changes when relevant. Leave unticked only if none of those surfaces was touched.
  • 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

  • New Features

    • Enhanced scroll position recovery using timeline anchors for improved message visibility during navigation
    • Weak upward gesture recognition to prevent accidental scrolling while composing
  • Bug Fixes

    • Improved scroll restoration stability when timeline content changes or layout shifts occur
    • Protected scroll position during message submission to prevent unintended upward scrolling
  • Tests

    • Expanded scroll anchor coverage with multiple restoration and visibility scenarios
    • Added weak gesture detection test cases with diagnostic verification

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Adds DOM reading anchors and richer reading-position data, preserves latest during layout settling by ignoring weak upward gestures, exposes a dock layout-change preservation callback, and validates behavior with unit tests and an E2E diagnostic-capturing test.

Changes

Timeline Scroll Anchoring and Latest Protection

Layer / File(s) Summary
Reading anchor type & diagnostic contracts
packages/app/src/pages/session/session-timeline-scroll-controller.ts
Adds exported TimelineReadingAnchorScope / TimelineReadingAnchor, enriches TimelineSafePosition reading variant, adds latest_protected_weak_upward_ignored reason, and extends diagnostic fields.
Anchor discovery and restore logic
packages/app/src/pages/session/session-timeline-scroll-anchors.ts, packages/app/src/pages/session/session-timeline-scroll-anchors.test.ts
Discover data-timeline-anchor elements, select primary/fallback anchors and fallback message offsets, return richer reading positions from sampler, and restore by locating/measuring anchors or falling back to message rects. Tests cover sampling and multiple restore fallback scenarios.
Scroll controller cloning, diagnostics, weak-upward handling
packages/app/src/pages/session/session-timeline-scroll-controller.ts, packages/app/src/pages/session/session-timeline-scroll-controller.test.ts
Deep-clone reading-anchor fields in state snapshots, populate anchor_scope/preserve_strategy in diagnostics, add isWeakUpwardTimelineIntent, and ignore weak upward wheel/touch intents when following_latest + latestProtected (emits latest_protected_weak_upward_ignored). Tests assert preserved latest protection and state snapshot isolation.
Dock-resize & layout-change preservation callback
packages/app/src/pages/session/use-session-scroll-dock.ts, packages/app/src/pages/session/use-session-scroll-dock.test.ts
createSessionScrollDock gains optional shouldPreserveLatestForLayoutChange callback; implementation collects pre-layout metrics and lets the callback override stick-to-bottom decisions for content/dock resize transactions. Unit test validates callback use and metrics.
Interaction layer wiring
packages/app/src/pages/session/use-session-timeline-interaction.ts, packages/app/src/pages/session/message-timeline.tsx
Imports isWeakUpwardTimelineIntent, exposes isLatestProtected helper, implements shouldPreserveLatestForLayoutChange decision, prevents bottom-follow lock cancellation for weak upward intents when latest is protected, and updates timeline handlers to respect controller result.
Unit tests & E2E diagnostic validation
packages/app/e2e/session/session-scroll-position.spec.ts, packages/desktop-electron/src/main/renderer-diagnostics-sanitize.ts, tests under packages/app/src/pages/session/*
Adds renderer diagnostics capture helpers and E2E test that streams an assistant reply, issues a weak upward wheel during completion, asserts latest remains pinned with constrained timeline movement, and checks diagnostics include latest_protected_weak_upward_ignored while omitting user-upward navigation; sanitizer updated to allow new fields and new layout_transaction event.

Sequence Diagram

sequenceDiagram
  participant User as User gesture
  participant Timeline as Timeline viewport
  participant Sampler as sampleTimelineSafePosition
  participant Finder as bestVisibleTimelineAnchor
  participant Controller as ScrollController
  participant Dock as ScrollDock

  User->>Timeline: weak upward wheel/touch
  Timeline->>Controller: intent(weak_upward)
  Controller->>Controller: check following_latest + latestProtected
  alt latestProtected active
    Controller-->>Timeline: ignore intent (latest_protected_weak_upward_ignored)
  else not protected
    Controller-->>Timeline: accept intent -> scroll
  end

  Dock->>Dock: layout change starts (dock/content resize)
  Dock->>Dock: collect pre-layout metrics (scrollTop,distanceFromBottom,...)
  Dock->>Controller: shouldPreserveLatestForLayoutChange(event, metrics)
  Controller-->>Dock: decision -> apply stickToBottom
  Note over Sampler,Finder: sampling & restore
  Timeline->>Sampler: sampleTimelineSafePosition()
  Sampler->>Finder: query data-timeline-anchor elements
  Finder-->>Sampler: primary + fallback anchors
  Timeline->>Controller: store reading position keys
  Controller->>Timeline: restoreReading() -> locate anchors -> compute scrollTop -> restore
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

  • #595 — Similar consolidation of reading-anchor metadata, weak-upward intent handling, diagnostics, and dock/interaction updates.

Possibly related PRs

Suggested labels

P1

"🐰 I hop through anchors, small and bright,
I guard the latest through the tiny up-swipe.
When layouts shrink and the dock takes its toll,
I keep the reading line steady — that’s my goal.
Hooray for anchors, snug and tight! 🥕"

🚥 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 'fix(app): stabilize timeline scroll recovery' directly and clearly summarizes the main change: fixing timeline scroll behavior during recovery operations.
Description check ✅ Passed The PR description is comprehensive, following the template structure with Summary, Why, Related Issue, Human Review Status, Review Focus, Risk Notes, How To Verify, and Screenshots sections. All required sections are present and well-filled.
Linked Issues check ✅ Passed The PR directly addresses issue #911 by implementing fine-grained reading-anchor sampling with fallback recovery, latest-protection during layout settling, and comprehensive test coverage for anchor fallback scenarios and weak-upward gesture handling.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the stated objectives: reading-anchor sampling in timeline-scroll-anchors, latest-protection in scroll-controller, layout transaction wiring in use-session-timeline-interaction, E2E and unit test coverage, and sanitizer allowlisting for new diagnostic fields.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pawwork/fix-911-scroll-stability

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.

@Astro-Han Astro-Han added bug Something isn't working app Application behavior and product flows P2 Medium priority labels May 26, 2026

@github-actions github-actions 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.

Suggested priority: P2 (includes user-path files (packages/app/src/pages/session/session-timeline-scroll-anchors.test.ts, packages/app/src/pages/session/session-timeline-scroll-anchors.ts, packages/app/src/pages/session/session-timeline-scroll-controller.test.ts, packages/app/src/pages/session/session-timeline-scroll-controller.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)).

P1/P0 are reserved for maintainer confirmation. Please relabel manually if this is a release blocker, security issue, data-loss risk, or updater/runtime failure.

@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 timeline scroll anchor sampling and restoration to preserve the user's reading position across layout changes, such as dock resizing. It also adds logic to ignore weak upward scroll intents to protect the 'latest' message state during layout settling. The feedback focuses on performance optimizations (avoiding layout thrashing by checking rect dimensions before calling getComputedStyle), standardizing DOM queries using closest and attribute selectors, and removing duplicated logic by exporting and reusing the isWeakUpwardTimelineIntent helper.

Comment thread packages/app/src/pages/session/session-timeline-scroll-anchors.ts Outdated
Comment thread packages/app/src/pages/session/session-timeline-scroll-anchors.ts Outdated
Comment thread packages/app/src/pages/session/session-timeline-scroll-anchors.ts
Comment thread packages/app/src/pages/session/session-timeline-scroll-controller.ts Outdated
Comment thread packages/app/src/pages/session/use-session-timeline-interaction.ts Outdated
@github-actions

github-actions Bot commented May 26, 2026

Copy link
Copy Markdown

Perf delta summary

Comparator: pass

Profile / Scenario interaction median interaction worst long task max tbt frame gap p95 frame gap max jank count cls status
default / homepage-cold 24 -> 24 (0) 40 -> 40 (0) 86 -> 85 (-1) 36 -> 35 (-1) 16.8 -> 16.8 (0) 133.3 -> 116.6 (-16.7) 4 -> 3 (-1) 0 -> 0 (0) pass
default / long-session-input-lag 48 -> 48 (0) 48 -> 64 (+16) 0 -> 0 (0) 0 -> 0 (0) 16.8 -> 16.7 (-0.1) 16.8 -> 16.8 (0) 0 -> 0 (0) 0 -> 0 (0) pass
default / session-streaming-long 48 -> 56 (+8) 72 -> 64 (-8) 0 -> 0 (0) 0 -> 0 (0) 16.7 -> 16.8 (+0.1) 33.4 -> 33.3 (-0.1) 0 -> 0 (0) 0 -> 0 (0) pass
default / tool-call-expand 40 -> 40 (0) 40 -> 40 (0) 0 -> 0 (0) 0 -> 0 (0) 16.7 -> 16.8 (+0.1) 16.7 -> 16.8 (+0.1) 0 -> 0 (0) 0 -> 0 (0) pass
default / tool-default-open-heavy-bash 16 -> 24 (+8) 24 -> 24 (0) 99 -> 95 (-4) 62 -> 63 (+1) 49.9 -> 66.6 (+16.7) 116.7 -> 116.6 (-0.1) 3 -> 4 (+1) 0.004 -> 0.004 (0) pass
default / terminal-side-panel-open 56 -> 48 (-8) 64 -> 56 (-8) 0 -> 0 (0) 0 -> 0 (0) 33.4 -> 33.4 (0) 33.4 -> 33.4 (0) 0 -> 0 (0) 0 -> 0 (0) pass
default / session-scroll-reading 16 -> 16 (0) 16 -> 32 (+16) 0 -> 0 (0) 0 -> 0 (0) 16.7 -> 33.3 (+16.6) 16.7 -> 33.3 (+16.6) 0 -> 0 (0) 0 -> 0 (0) pass
low-end / session-scroll-reading-long 0 -> 0 (0) 0 -> 0 (0) 54 -> 53 (-1) 4 -> 3 (-1) 33.3 -> 33.3 (0) 66.7 -> 50 (-16.7) 1 -> 0 (-1) 0 -> 0 (0) pass
low-end / session-timeline-recompute 40 -> 40 (0) 48 -> 48 (0) 0 -> 0 (0) 0 -> 0 (0) 33.3 -> 33.4 (+0.1) 33.4 -> 33.4 (0) 0 -> 0 (0) 1.075 -> 1.075 (0) pass
low-end / concurrent-shimmer-extreme 0 -> 0 (0) 0 -> 0 (0) 0 -> 0 (0) 0 -> 0 (0) 16.7 -> 16.8 (+0.1) 16.8 -> 16.8 (0) 0 -> 0 (0) 0 -> 0 (0) pass

@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.

Actionable comments posted: 2

🤖 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.

Inline comments:
In `@packages/app/src/pages/session/session-timeline-scroll-anchors.ts`:
- Around line 262-278: The loop restoring using timelineAnchors may pick an
element that is hidden/collapsed (zero-size or inside a closed <details>),
producing invalid geometry; update the loop that uses timelineAnchors and
timelineAnchorByKey to: after resolving anchor, compute its geometry and skip it
if it's not visible (e.g., getClientRects().length === 0 or bounding rect has
zero width/height, or the anchor is inside a <details> whose
closest('details')?.open === false, or other standard visibility checks) so the
code falls through to the next anchor; only call setTimelineScrollTop when the
anchor passes these visibility checks. Ensure references to timelineAnchors,
timelineAnchorByKey, anchor.getBoundingClientRect(), and setTimelineScrollTop
are preserved so reviewers can find the change.

In `@packages/app/src/pages/session/session-timeline-scroll-controller.ts`:
- Around line 25-30: cloneState currently shallow-copies lastSafePosition and
pendingRecovery.anchor which lets callers mutate internal reading-anchor
objects; update cloneState to deep-clone the nested reading-anchor fields when
TimelineSafePosition.kind === "reading" (specifically clone primaryAnchor,
fallbackTrowAnchor, and fallbackMessage and its fields) and likewise deep-copy
pendingRecovery.anchor before returning state() so lastSafePosition and
pendingRecovery.anchor no longer share references with internal structures.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f5a5d6b9-9e70-458a-ad60-2e9c4fb91b8b

📥 Commits

Reviewing files that changed from the base of the PR and between ecb8cd2 and 7af8fff.

📒 Files selected for processing (8)
  • packages/app/e2e/session/session-scroll-position.spec.ts
  • packages/app/src/pages/session/session-timeline-scroll-anchors.test.ts
  • packages/app/src/pages/session/session-timeline-scroll-anchors.ts
  • packages/app/src/pages/session/session-timeline-scroll-controller.test.ts
  • packages/app/src/pages/session/session-timeline-scroll-controller.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

Comment thread packages/app/src/pages/session/session-timeline-scroll-anchors.ts
@github-actions github-actions Bot added ui Design system and user interface platform Electron shell, OS integration, packaging, updater, signing, paths, and permissions labels May 26, 2026

@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.

Actionable comments posted: 1

🤖 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.

Inline comments:
In `@packages/app/src/pages/session/session-timeline-scroll-anchors.ts`:
- Around line 110-119: The code currently falls back to scanning the whole
message even when a selectedBlock exists, which can pick a sibling block's trow
and restore the wrong position; change the scopes logic so that when
selectedBlock (from input.selected.closest(...)) is an HTMLElement you only
search that selectedBlock (scopes = [selectedBlock]) and do not perform a second
pass over message; keep using findFirstVisibleTrowAnchor with the same params
(scope, selectedKey, viewportRect) and if no anchor is found return undefined so
fallbackMessage can handle recovery instead of searching sibling blocks.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fac9f6d9-7fc6-4ea5-a2ba-1a4f534db77f

📥 Commits

Reviewing files that changed from the base of the PR and between 460c1f4 and 6bd9fe0.

📒 Files selected for processing (10)
  • packages/app/e2e/session/session-scroll-position.spec.ts
  • packages/app/src/pages/session/message-timeline.tsx
  • packages/app/src/pages/session/session-timeline-scroll-anchors.test.ts
  • packages/app/src/pages/session/session-timeline-scroll-anchors.ts
  • packages/app/src/pages/session/session-timeline-scroll-controller.test.ts
  • packages/app/src/pages/session/session-timeline-scroll-controller.ts
  • packages/app/src/pages/session/use-session-timeline-interaction.test.ts
  • packages/app/src/pages/session/use-session-timeline-interaction.ts
  • packages/desktop-electron/src/main/renderer-diagnostics-sanitize.test.ts
  • packages/desktop-electron/src/main/renderer-diagnostics-sanitize.ts

Comment on lines +110 to +119
const selectedBlock = input.selected.closest('[data-component="session-turn-trow-block"]')
const scopes = selectedBlock instanceof HTMLElement ? [selectedBlock, message] : [message]
for (const scope of scopes) {
const anchor = findFirstVisibleTrowAnchor({
scope,
selectedKey: input.selectedKey,
viewportRect: input.viewportRect,
})
if (anchor) return anchor
}

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.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't widen the trow fallback back to the whole message.

When selectedBlock exists, Lines 111-118 still do a second pass over message. If the chosen tool row has no visible trow:* anchor in its own block, this can capture a sibling block's trow and restore to the wrong intra-message position. In that case it's safer to return undefined and let fallbackMessage handle recovery.

Suggested fix
   const selectedBlock = input.selected.closest('[data-component="session-turn-trow-block"]')
-  const scopes = selectedBlock instanceof HTMLElement ? [selectedBlock, message] : [message]
-  for (const scope of scopes) {
-    const anchor = findFirstVisibleTrowAnchor({
-      scope,
-      selectedKey: input.selectedKey,
-      viewportRect: input.viewportRect,
-    })
-    if (anchor) return anchor
-  }
+  const scope = selectedBlock instanceof HTMLElement ? selectedBlock : message
+  return findFirstVisibleTrowAnchor({
+    scope,
+    selectedKey: input.selectedKey,
+    viewportRect: input.viewportRect,
+  })
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const selectedBlock = input.selected.closest('[data-component="session-turn-trow-block"]')
const scopes = selectedBlock instanceof HTMLElement ? [selectedBlock, message] : [message]
for (const scope of scopes) {
const anchor = findFirstVisibleTrowAnchor({
scope,
selectedKey: input.selectedKey,
viewportRect: input.viewportRect,
})
if (anchor) return anchor
}
const selectedBlock = input.selected.closest('[data-component="session-turn-trow-block"]')
const scope = selectedBlock instanceof HTMLElement ? selectedBlock : message
return findFirstVisibleTrowAnchor({
scope,
selectedKey: input.selectedKey,
viewportRect: input.viewportRect,
})
🤖 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/session-timeline-scroll-anchors.ts` around
lines 110 - 119, The code currently falls back to scanning the whole message
even when a selectedBlock exists, which can pick a sibling block's trow and
restore the wrong position; change the scopes logic so that when selectedBlock
(from input.selected.closest(...)) is an HTMLElement you only search that
selectedBlock (scopes = [selectedBlock]) and do not perform a second pass over
message; keep using findFirstVisibleTrowAnchor with the same params (scope,
selectedKey, viewportRect) and if no anchor is found return undefined so
fallbackMessage can handle recovery instead of searching sibling blocks.

@Astro-Han

Copy link
Copy Markdown
Owner Author

Closing this PR without merging.

Follow-up manual testing on May 26 showed that the timeline still jitters, especially around tool row expand/collapse and content resize. The latest exported renderer diagnostics (pawwork-renderer-diagnostics-2026-05-26-12-01-27.json) confirms that this is not a single remaining edge case:

  • After a user scroll, the controller can remain in following_latest; a subsequent content-resize layout transaction then restores the latest anchor and pulls the viewport back to bottom.
  • Around tool/content layout changes, scroll samples show large jumps in scrollHeight, scrollTop, and distance_from_bottom, including transitions such as 3369 -> 905 -> 3194 in scroll height.
  • During reading-history interaction, repeated content-resize transactions continue while anchors switch between message/trow scopes and even between message IDs, producing visible position instability.

This PR accumulated 11 commits across several patches:

  1. fix(app): stabilize timeline scroll recovery
  2. test(app): cover timeline latest weak-wheel stability
  3. test(app): cover timeline anchor rekey fallback
  4. fix(app): ignore hidden timeline restore anchors
  5. fix(app): restore shifted timeline anchors
  6. fix(app): preserve closed trow summary anchors
  7. refactor(app): share weak timeline intent helper
  8. fix(app): scope trow fallback to selected block
  9. fix(app): suppress ignored latest scroll gestures
  10. fix(desktop): export timeline layout diagnostics
  11. fix(app): preserve explicit timeline scroll intent

That history is a useful investigation trail, but it is also the reason not to merge it: the fix strategy is now too patch-oriented for a core timeline ownership problem. The next pass should start from a small design/architecture reset around timeline scroll ownership, with explicit boundaries for user intent, latest following, layout transactions, tool/trow anchor restoration, and who is allowed to issue scroll writes.

@Astro-Han

Copy link
Copy Markdown
Owner Author

Closing as not planned. Follow-up diagnostics and manual testing show the scroll jitter remains, and this PR has become a patch stack rather than a mergeable architecture fix.

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 P2 Medium priority platform Electron shell, OS integration, packaging, updater, signing, paths, and permissions ui Design system and user interface

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Timeline scroll ownership breaks during tool and layout changes

1 participant