Skip to content

Fix session timeline scroll containment - #1362

Merged
ColeMurray merged 2 commits into
mainfrom
fix/session-scrolling-containment
Aug 11, 2026
Merged

Fix session timeline scroll containment#1362
ColeMurray merged 2 commits into
mainfrom
fix/session-scrolling-containment

Conversation

@ColeMurray

@ColeMurray ColeMurray commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Summary

  • contain the resizable timeline and desktop workspace panels so their implicit content wrappers cannot become competing scroll containers
  • add min-h-0 through the session flex hierarchy and use overflow-clip at the session root
  • run bottom-following in a layout effect for both event and processing-state changes while keeping history-prepend compensation in its own earlier layout effect
  • add focused regressions for panel overflow containment and processing-indicator height changes

Why PR #1340 was incomplete

PR #1340 correctly removed streaming-triggered scrollIntoView calls and scoped auto-follow writes to timeline.scrollTop. That prevented direct page-level scrolling, but it did not eliminate the other scroll containers surrounding the timeline.

react-resizable-panels v4 wraps each Panel child in an implicit element whose default overflow: auto can make it independently scrollable. When nested sub-task activity grows beneath an earlier Task row, browser scroll anchoring can adjust those panel wrappers even though the timeline code only writes to the intended timeline element. This is most visible while a sub-task is thinking because activity is inserted retroactively rather than only appended at the end.

The timeline also performed bottom-following in a passive effect keyed only by events. Toggling isProcessing adds or removes the Thinking indicator without necessarily changing the event array, so its height change was not corrected before paint. The follow effect now uses useLayoutEffect and depends on both events and isProcessing.

History prepend compensation remains a separate layout effect that runs first. This preserves the reader's viewport and marks that render so the following bottom-follow effect does not race the prepend adjustment when processing state changes at the same time.

Testing

  • npm test -w @open-inspect/web -- src/components/session-timeline.test.tsx src/components/session-desktop-layout.test.tsx (29 passed)
  • npm run typecheck -w @open-inspect/web
  • npm run lint -w @open-inspect/web
  • git diff --check

The unrelated pre-existing package-lock.json modification is not included.


Created with Open-Inspect

Summary by CodeRabbit

  • Bug Fixes

    • Improved session workspace sizing and overflow handling to keep content contained within its panels.
    • Improved timeline scrolling so new activity and processing updates remain visible without disrupting the user’s current position.
  • Tests

    • Added coverage for workspace overflow and nested panel behavior.
    • Added coverage for timeline auto-scrolling, processing updates, and preserving the user’s scroll position.

@github-actions

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format
Init
Validate

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @ColeMurray, Action: pull_request

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The session UI now constrains layout overflow and minimum dimensions. Timeline scroll adjustment runs during layout and reevaluates when processing starts. Tests cover panel styles, scrolling, user-position preservation, and processing-indicator height changes.

Changes

Session UI behavior

Layer / File(s) Summary
Session layout containment
packages/web/src/app/(app)/session/[id]/page.tsx, packages/web/src/components/session-desktop-layout.tsx, packages/web/src/components/session-desktop-layout.test.tsx
Session containers and the main panel enforce minimum dimensions and clipped overflow. Tests verify panel styles and resizable-panel setup.
Processing-state timeline scrolling
packages/web/src/components/session-timeline.tsx, packages/web/src/components/session-timeline-scroll.test.tsx, packages/web/src/components/session-timeline.test.tsx
Timeline scrolling uses useLayoutEffect and reruns when isProcessing changes. Tests cover automatic scrolling, user-position preservation, and processing-indicator height changes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: open-inspect

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main changes to session timeline scrolling and layout containment.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ 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 fix/session-scrolling-containment

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.

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

Summary

Reviewed PR #1362, “Fix session timeline scroll containment” by @ColeMurray. The PR changes 5 files with +51/-8 and keeps the fix narrowly focused on scroll containment and timeline bottom-follow behavior.

Critical Issues

None found.

Suggestions

None.

Nitpicks

None.

Positive Feedback

  • The panel overflow containment is applied at the resizable panel boundary where the competing scroll container is introduced.
  • Moving the bottom-follow adjustment to a layout effect and including isProcessing covers the indicator height-change case without mixing it into the history-prepend compensation effect.
  • The added regression tests are focused on the changed behavior.

Questions

None.

Verification

  • Reviewed gh pr diff 1362 and the fetched PR head diff.
  • git diff --check main...origin/pr-1362 passed.
  • Focused web tests could not be run locally because dependencies were unavailable in the PR worktree (vitest was not installed).

Verdict

Approve.

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

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/web/src/components/session-timeline.test.tsx`:
- Around line 605-613: Update the mocked timeline element in the test around
SessionTimeline to define a scrollTop setter that clamps assigned values to
scrollHeight minus clientHeight, matching browser behavior. Keep the existing
clientHeight and scrollHeight values, and change the assertion to expect the
clamped position of 800.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7625a167-8fbd-4f7e-a8e0-c7d6d532bdd1

📥 Commits

Reviewing files that changed from the base of the PR and between d635d23 and 9be6655.

📒 Files selected for processing (5)
  • packages/web/src/app/(app)/session/[id]/page.tsx
  • packages/web/src/components/session-desktop-layout.test.tsx
  • packages/web/src/components/session-desktop-layout.tsx
  • packages/web/src/components/session-timeline.test.tsx
  • packages/web/src/components/session-timeline.tsx

Comment thread packages/web/src/components/session-timeline.test.tsx Outdated
open-inspect[bot]
open-inspect Bot previously requested changes Aug 10, 2026

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

[deep review] Requesting changes because the panel fix still leaves hidden-overflow ancestors as scroll containers, while the regression tests mock away the relevant DOM and effect timing. The new timeline test also pushes an already-large test file from 999 to 1,015 lines; the focused scroll suite should be decomposed rather than growing this monolith.

Comment thread packages/web/src/app/(app)/session/[id]/page.tsx Outdated
Comment thread packages/web/src/components/session-desktop-layout.test.tsx Outdated
Comment thread packages/web/src/components/session-timeline.test.tsx Outdated
@github-actions

Copy link
Copy Markdown

Terraform Validation Results

Step Status
Format
Init
Validate

Note: Terraform plan was skipped because secrets are not configured. This is expected for external contributors. See docs/GETTING_STARTED.md for setup instructions.

Pushed by: @open-inspect[bot], Action: pull_request

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

🧹 Nitpick comments (1)
packages/web/src/components/session-timeline-scroll.test.tsx (1)

41-58: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Name the default helper argument.

Line 45 defines the default value inline. Declare the empty value once as a named constant and use it for extra.

Proposed change
+const EMPTY_TOOL_EVENT_EXTRA: Partial<Extract<SandboxEvent, { type: "tool_call" }>> = {};
+
 function toolEvent(
   tool: string,
   callId: string,
   timestamp: number,
-  extra: Partial<Extract<SandboxEvent, { type: "tool_call" }>> = {}
+  extra: Partial<Extract<SandboxEvent, { type: "tool_call" }>> = EMPTY_TOOL_EVENT_EXTRA
 ): Extract<SandboxEvent, { type: "tool_call" }> {

As per coding guidelines, define each TypeScript default value exactly once as a named constant and import it wherever needed.

🤖 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/web/src/components/session-timeline-scroll.test.tsx` around lines 41
- 58, Update the toolEvent helper to define the empty extra-value object once as
a named constant, then use that constant as the default for extra instead of the
inline literal. Keep the helper’s returned event behavior unchanged.

Source: Coding guidelines

🤖 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/web/src/components/session-timeline-scroll.test.tsx`:
- Around line 41-58: Update the toolEvent helper to define the empty extra-value
object once as a named constant, then use that constant as the default for extra
instead of the inline literal. Keep the helper’s returned event behavior
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d2f8e6a3-06d8-457a-beb6-24ceeeafb1d2

📥 Commits

Reviewing files that changed from the base of the PR and between 9be6655 and 31b715f.

📒 Files selected for processing (5)
  • packages/web/src/app/(app)/session/[id]/page.tsx
  • packages/web/src/components/session-desktop-layout.test.tsx
  • packages/web/src/components/session-desktop-layout.tsx
  • packages/web/src/components/session-timeline-scroll.test.tsx
  • packages/web/src/components/session-timeline.test.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/web/src/components/session-desktop-layout.tsx
  • packages/web/src/app/(app)/session/[id]/page.tsx
  • packages/web/src/components/session-desktop-layout.test.tsx

@ColeMurray

Copy link
Copy Markdown
Owner Author

@open-inspect[bot] please re-review and approve

@open-inspect

open-inspect Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Re-reviewed PR #1362 and approved it. No blocking findings found. Verification: focused web tests passed locally (27 tests), and the PR status checks are green.

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

Re-reviewed the latest revision. The prior concern is resolved: the scroll regression coverage is now in a focused suite, the layout test uses the real resizable panel components, and overflow/min-size containment remains targeted at the relevant panel boundaries. No blocking findings.

@ColeMurray
ColeMurray merged commit 9a5403a into main Aug 11, 2026
19 checks passed
@ColeMurray
ColeMurray deleted the fix/session-scrolling-containment branch August 11, 2026 07:20
ColeMurray added a commit that referenced this pull request Aug 13, 2026
## Summary

One-word layout fix: add `relative` to the session timeline scroll
container so it is the **containing block** for absolutely-positioned
descendants, plus a structural regression test.

## Symptom

While sub-agent tasks are running, the page itself becomes scrollable:
you can scroll past the application bounds and the entire layout
(header, sidebar, everything) moves. The moment the tasks finish, the
symptom disappears. Long task activity makes it dramatically worse.

## Root cause

`TaskActivityItem` renders a screen-reader live status while a task is
running:

```tsx
<span role="status" className="sr-only">Task in progress</span>
```

Tailwind's `sr-only` is `position: absolute`. **No ancestor in the
session layout chain is positioned**, so the span's containing block is
the initial containing block — the document. That has two consequences:

1. It escapes every `overflow: clip` / `overflow: hidden` barrier
between the timeline and `<body>` (overflow clipping does not apply to
absolutely-positioned descendants whose containing block is outside the
clipper).
2. It is laid out at its static position measured in the timeline's
**content** flow, and does not move with the timeline's `scrollTop` (the
scroller is not its containing block).

With a long session, the running task's row sits thousands of pixels
down the timeline content — so the span lands thousands of pixels down
the **document**, giving `<html>` real scrollable overflow. Wheel input
over the timeline chains into the document once the timeline hits
bottom, scrolling the whole layout. When the task completes the span
unmounts and the overflow collapses, which is why the bug is transient
and only reproduces while tasks are thinking.

This is why the previous containment passes (#1340, #1362, #1396) never
caught it: they hardened the overflow/height chain, which
absolute-to-ICB content bypasses entirely, and the jsdom tests cannot
observe real layout.

## Evidence

Measured live against the deployed app (CDP attached to Chrome) during a
parallel sub-task run:

- `html.scrollHeight` grew to **7004px** against a 1464px viewport while
`scrollTop` was still 0 — real document overflow, growing +32px per
streamed row (each new row pushes the running task's static position
down).
- 7004 ≈ the running task's y-position in the 7049px timeline content —
independent of the timeline's scroll offset.
- The only absolutely-positioned elements in timeline content are this
span (running tasks only) and the screenshot-card play overlay (already
contained by a `relative` parent).

Reproduced locally in headless Chrome with a 150-row history before a
running task:

| | page scrollHeight / clientHeight | page scrollTop after wheeling
past timeline bottom |
|---|---|---|
| before fix | 4895 / 813 | **4082** (layout scrolls away) |
| after fix | 813 / 813 | 0 |

After the fix, a 10,000px expanded task activity stays fully inside the
timeline scroller across every configuration (base, terminal open,
changes open, narrow viewport, streaming) — no nested scrollbar or
activity height cap needed; expand/collapse behaves like a normal
disclosure.

## Changes

- `session-timeline.tsx`: `relative` on the timeline scroll container,
with a comment explaining the invariant.
- `session-timeline-scroll.test.tsx`: regression test asserting the
scroller is the containing block for the running-task status span.

## Testing

- `npm test -w @open-inspect/web --
src/components/session-timeline-scroll.test.tsx
src/components/session-timeline.test.tsx
src/components/task-activity-item.test.tsx
src/components/session-desktop-layout.test.tsx` (37 passed)
- `npm run typecheck -w @open-inspect/web`
- ESLint on changed files
- Headless-Chrome layout verification described above (real browser,
real components, fabricated events)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Fixed live-status elements in the session timeline so they remain
properly contained within the scroll area during sub-task streaming.
* **Tests**
* Added regression coverage to verify correct timeline scrolling
behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant