Skip to content

feat(browser): scroll nested containers in browser_scroll - #92

Open
happinessisreal wants to merge 1 commit into
omdsh-dev:mainfrom
happinessisreal:feat/browser-scroll-nested-container
Open

happinessisreal wants to merge 1 commit into
omdsh-dev:mainfrom
happinessisreal:feat/browser-scroll-nested-container

Conversation

@happinessisreal

@happinessisreal happinessisreal commented Sep 16, 2026

Copy link
Copy Markdown

Problem

browser_scroll only ever called window.scrollTo / window.scrollBy, so it could not move content held in a nested scroll container. Fixed-height app shells - chat transcripts, side panes, modal bodies - keep their content in an inner overflow: auto element, so the window itself has no overflow: the page-level scroll is a no-op, and that container never receives the scroll event its lazy loading waits on.

Concretely, on Instagram web DMs the conversation pane unloads everything older than the newest few messages, and the pane only moves on a real wheel gesture. Text-only browsing therefore stops at whatever the app happens to have mounted, with no way to reach the history above it.

Change

browser_scroll accepts an optional index (element index from browser_snapshot) or selector (CSS selector). The action resolves the nearest scrollable ancestor of that anchor - the anchor itself included - moves that container's scrollTop, and then replays a wheel gesture. A synthetic wheel event does not scroll anything by itself, but apps commonly gate lazy loading on it, so the gesture is replayed after the position moves. The existing waitForPageSettled / withPageDelta path then returns the newly mounted content automatically.

Behaviour:

  • No target: the previous page-level behaviour, unchanged.
  • Target with no scrollable container: falls back to the page and says so in the status line.
  • Both index and selector: rejected as bad-args.
  • Invalid CSS selector: rejected as bad-args; selector matching nothing: action-failed.

Tests

  • extensions/dsh-browser/tests/actions-scroll.spec.ts (new, 6 cases): container scroll via element index, top/bottom edges, selector anchor, index+selector rejection, page fallback, and unchanged no-target behaviour.
  • packages/browser/bridge-browser/tests/tools.spec.ts: passthrough assertions for index and selector.

The new spec stubs scroll geometry and resolved overflow for the container only, because jsdom has no layout engine.

Verification

  • extensions/dsh-browser: tsc --noEmit clean; vitest run 381/381 passing across 49 files.
  • packages/browser/bridge-browser: tsc -p tsconfig.json --noEmit clean; vitest run 139/140, the single failure being token.spec.ts's POSIX mode assertion on Windows (0o600 vs 0o666), unrelated to this change.

RetriggerConfidence Score: 4/5

The nested-scrolling implementation is otherwise coherent, but the invalid empty-selector path should be fixed before merging because it can silently perform an unintended page scroll.

Findings

  1. P1 Empty Selectors Bypass Validation

Summary

This PR extends browser_scroll so callers can anchor scrolling by snapshot index or CSS selector, resolve the nearest vertically scrollable container, update its position, and emit a synthetic wheel event for application listeners.

  • Preserves document-level scrolling when no target is provided.
  • Adds nested-container resolution, page fallback, status reporting, and argument forwarding through the browser bridge.
  • Adds focused tests for indexed and selector targets, edge jumps, fallback behavior, mutual exclusion, and bridge passthrough.
  • Argument handling should reject an explicitly empty selector rather than treating it as omitted.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[browser_scroll request] --> B{Index or selector supplied?}
  B -- No --> C[Scroll document]
  B -- Yes --> D[Resolve anchor element]
  D --> E[Find nearest vertically scrollable ancestor]
  E --> F{Container found?}
  F -- No --> C
  F -- Yes --> G[Update container scrollTop]
  G --> H[Dispatch synthetic wheel event]
  C --> I[Wait for page to settle]
  H --> I
  I --> J[Return page delta and status]
Loading

Reviews (1) · Last reviewed commit: "feat(browser): scroll nested containers ..."

browser_scroll only ever called window.scrollTo/scrollBy, so it could not move an app pane whose own overflow container holds the content. Fixed-height app shells (chat transcripts, side panes, modal bodies) keep every message inside a nested scroller, so the window has no overflow to scroll and the container never receives the scroll event its lazy loading waits on.

browser_scroll now accepts an optional element index or CSS selector. The action resolves the nearest scrollable ancestor of that anchor and moves that container's scrollTop, then replays a wheel gesture because a synthetic wheel event does not scroll by itself while apps commonly gate lazy loading on it. Without a target the previous page-level behavior is unchanged; a target with no scrollable container falls back to the page and says so in the status line.
Comment on lines +450 to +453
const selector = typeof args.selector === 'string' && args.selector !== '' ? args.selector : undefined
const hasIndex = args.index !== undefined
if (hasIndex && selector !== undefined) {
throw new ActionError('bad-args', 'Provide either index or selector, not both.')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Empty selectors bypass validation

An explicitly supplied empty selector is converted to undefined before validation. As a result, { selector: "" } silently scrolls the page instead of returning bad-args, while { index: 7, selector: "" } bypasses the check that rejects both target fields and silently uses the index. The bridge schema permits empty strings and forwards them, so an invalid targeted request can unexpectedly scroll the page.

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