Skip to content

feat(tabs): scroll tab strip with the mouse wheel#1136

Open
peterjmorgan wants to merge 1 commit into
RunMaestro:rcfrom
peterjmorgan:feat/tab-bar-wheel-scroll
Open

feat(tabs): scroll tab strip with the mouse wheel#1136
peterjmorgan wants to merge 1 commit into
RunMaestro:rcfrom
peterjmorgan:feat/tab-bar-wheel-scroll

Conversation

@peterjmorgan

@peterjmorgan peterjmorgan commented Jun 27, 2026

Copy link
Copy Markdown

What

When the tab strip overflows, hovering over it and using the mouse wheel now pans the tabs horizontally (VS Code style). A plain vertical wheel (deltaY) is translated into horizontal scroll; trackpads and Shift+wheel emit deltaX, so the handler follows whichever axis the device reports.

Gated behind a new Scroll tabs with the mouse wheel toggle in Settings -> Display (tabBarWheelScroll, default on).

Why

With a normal mouse (no horizontal wheel), there was no quick way to reach off-screen tabs without dragging or using keyboard shortcuts. This matches the tab-strip behavior users expect from VS Code and modern browsers.

How

  • TabBar.tsx: reads the new setting and attaches a native, non-passive wheel listener to the existing scroll container. It only acts when the strip actually overflows (scrollWidth > clientWidth), adjusts scrollLeft, and calls preventDefault() so the wheel doesn't also scroll an outer container.
    • The listener is attached natively with { passive: false } because React's synthetic onWheel runs passively and cannot call preventDefault().
  • settingsStore.ts: new tabBarWheelScroll boolean (default true) + setter + load-from-disk patch, mirroring the existing showBrowserTabDomain tab setting.
  • useSettings.ts: added field + setter to UseSettingsReturn (the hook spreads ...store).
  • DisplayTab.tsx: new toggle in the tab-display section.

Testing

  • TabBar.test.tsx + DisplayTab.test.tsx: 272 passed.
  • ESLint clean on all four changed files.
  • Type-check clean on all four changed files.

Summary by CodeRabbit

  • New Features
    • Added a “Scroll tabs with the mouse wheel” option to Display settings, persisted across sessions (default: on) for overflowed tabs.
    • Added a toggle switch to enable or disable this behavior.
  • Bug Fixes
    • Improved tab-strip wheel handling by converting wheel movement into horizontal scrolling, preferring the dominant wheel axis, and preventing the wheel from scrolling surrounding content when enabled.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

A persisted tabBarWheelScroll setting is added to the settings store, exposed through useSettings, used by TabBarInner to enable native wheel-based horizontal tab scrolling, and toggled from the Display settings tab.

Tab Bar Wheel Scroll

Layer / File(s) Summary
Store state, actions, and hook interface
src/renderer/stores/settingsStore.ts, src/renderer/hooks/settings/useSettings.ts
Adds tabBarWheelScroll to the store state and actions, implements persistence and hydration, sets the default to true, and exposes the field and setter in UseSettingsReturn.
Wheel listener in TabBarInner
src/renderer/components/TabBar/TabBar.tsx
Reads tabBarWheelScroll from the store and conditionally attaches a non-passive native wheel listener that pans scrollLeft using the dominant wheel axis and calls preventDefault; the listener is removed on cleanup.
Display settings toggle
src/renderer/components/Settings/tabs/DisplayTab.tsx
Destructures tabBarWheelScroll and setTabBarWheelScroll from useSettings and renders a labeled switch row in the Display tab.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Poem

🐇 Hop, hop, the tabs go swoosh,
A tiny wheel gives them a push,
One toggle on, one scrolls away,
The tab strip dances day by day,
Hooray! My whiskers do a jig 🐰

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding mouse-wheel scrolling for the tab strip.
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 unit tests (beta)
  • Create PR with unit tests

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.

@greptile-apps

greptile-apps Bot commented Jun 27, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds mouse-wheel scrolling for overflowing tab strips. The main changes are:

  • A new tabBarWheelScroll setting in the renderer store and settings hook.
  • A Display settings toggle for enabling or disabling tab-strip wheel scrolling.
  • A native non-passive wheel listener on the tab bar to translate wheel input into horizontal panning.
  • An unrelated renderer default change for the web interface custom port.

Confidence Score: 4/5

The custom-port default mismatch should be fixed before merging.

  • Fresh installs can show 9080 in the renderer while the web server still uses 8080.
  • The tab-wheel behavior is mostly contained, but it can consume wheel input at scroll edges.
  • The new setting works locally, but it is not discoverable through settings search.

src/renderer/stores/settingsStore.ts, src/renderer/components/TabBar/TabBar.tsx, src/renderer/components/Settings/tabs/DisplayTab.tsx

Important Files Changed

Filename Overview
src/renderer/components/Settings/tabs/DisplayTab.tsx Adds the Display settings toggle for tab-bar wheel scrolling, but the new control is missing its own searchable settings surface.
src/renderer/components/TabBar/TabBar.tsx Adds the native wheel listener for tab-strip panning, with an edge case where wheel events are consumed after scrolling is already clamped.
src/renderer/hooks/settings/useSettings.ts Extends the settings hook interface with the new tab-bar wheel-scroll value and setter.
src/renderer/stores/settingsStore.ts Adds the new setting state, setter, default, and load patch, but also changes the renderer-only web interface port default to a value that disagrees with the main process.

Reviews (1): Last reviewed commit: "feat(tabs): scroll tab strip with the mo..." | Re-trigger Greptile

Comment thread src/renderer/stores/settingsStore.ts Outdated
persistentWebLink: false,
webInterfaceUseCustomPort: false,
webInterfaceCustomPort: 8080,
webInterfaceCustomPort: 9080,

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 Renderer Port Default Diverges

Changing only the renderer default to 9080 leaves the main settings defaults, shared metadata, and web-server fallback at 8080. On a fresh install or reset with no persisted webInterfaceCustomPort, the UI can show 9080 while the server still starts on 8080, so users who enable the custom port without retyping it get a broken URL.

const delta = Math.abs(e.deltaX) > Math.abs(e.deltaY) ? e.deltaX : e.deltaY;
if (delta === 0) return;
el.scrollLeft += delta;
e.preventDefault();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Wheel Events Trap At Edges

When the tab strip overflows and is already at the left or right scroll limit, el.scrollLeft += delta is clamped and produces no movement, but this line still consumes the wheel event. A vertical wheel over the tab bar can then stop the surrounding content from scrolling even though the tab strip could not pan further.

Comment on lines 1185 to +1215
</div>

{/* Scroll Tab Strip With Mouse Wheel */}
<div
className="flex items-center justify-between pt-3 border-t"
style={{ borderColor: theme.colors.border }}
>
<div>
<p className="text-sm" style={{ color: theme.colors.textMain }}>
Scroll tabs with the mouse wheel
</p>
<p className="text-xs opacity-50 mt-0.5">
When the tab strip overflows, hover over it and scroll the mouse wheel to pan the
tabs left and right. Disable to ignore the wheel over the tab bar.
</p>
</div>
<button
onClick={() => setTabBarWheelScroll(!tabBarWheelScroll)}
className="relative w-10 h-5 rounded-full transition-colors flex-shrink-0 outline-none"
tabIndex={0}
style={{
backgroundColor: tabBarWheelScroll ? theme.colors.accent : theme.colors.bgActivity,
}}
role="switch"
aria-checked={tabBarWheelScroll}
aria-label="Scroll tabs with the mouse wheel"
>
<span
className={`absolute left-0 top-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
tabBarWheelScroll ? 'translate-x-5' : 'translate-x-0.5'
}`}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Setting Is Not Searchable

This adds a user-facing setting without its own data-setting-id or matching search/metadata entry. Users searching Settings for wheel, scroll, or tab will not be taken to this toggle, and metadata-driven settings surfaces will not know the new setting exists.

Context Used: CLAUDE.md (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@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)
src/renderer/components/TabBar/TabBar.tsx (1)

155-173: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use useEventListener() for the wheel binding.

This manual addEventListener/removeEventListener pair diverges from the renderer standard. Please wire this through useEventListener() and keep the { passive: false } option there instead of managing the native listener inside useEffect. As per coding guidelines, src/renderer/**/*.{ts,tsx} should “Use useEventListener() from src/renderer/hooks/utils/useEventListener.ts for event listener management. Do not pair raw addEventListener/removeEventListener inside useEffect.”

🤖 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 `@src/renderer/components/TabBar/TabBar.tsx` around lines 155 - 173, The wheel
handler in TabBar should be managed through the renderer’s shared event-listener
hook instead of a manual native subscription. Update the TabBar component to use
`useEventListener()` from `src/renderer/hooks/utils/useEventListener.ts` for the
tab strip wheel binding, preserving the current `tabBarWheelScroll` gating,
`handleWheel` logic, and `{ passive: false }` option there rather than inside
`useEffect`. This should remove the direct
`addEventListener`/`removeEventListener` pairing from `TabBar.tsx` while keeping
the same scroll behavior.

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 `@src/renderer/components/TabBar/TabBar.tsx`:
- Around line 155-173: The wheel handler in TabBar should be managed through the
renderer’s shared event-listener hook instead of a manual native subscription.
Update the TabBar component to use `useEventListener()` from
`src/renderer/hooks/utils/useEventListener.ts` for the tab strip wheel binding,
preserving the current `tabBarWheelScroll` gating, `handleWheel` logic, and `{
passive: false }` option there rather than inside `useEffect`. This should
remove the direct `addEventListener`/`removeEventListener` pairing from
`TabBar.tsx` while keeping the same scroll behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dde75335-8d0a-4df8-b0b7-b2466aafd0ce

📥 Commits

Reviewing files that changed from the base of the PR and between b330223 and d61dd74.

📒 Files selected for processing (4)
  • src/renderer/components/Settings/tabs/DisplayTab.tsx
  • src/renderer/components/TabBar/TabBar.tsx
  • src/renderer/hooks/settings/useSettings.ts
  • src/renderer/stores/settingsStore.ts

When the tab strip overflows, hovering over it and using the mouse
wheel now pans the tabs horizontally (VS Code style). A plain vertical
wheel (deltaY) is translated into horizontal scroll; trackpads and
Shift+wheel emit deltaX, so the handler follows whichever axis the
device reports.

The listener is attached natively with { passive: false } because
React's synthetic onWheel runs passively and cannot call
preventDefault, which is needed to stop an outer container from also
scrolling.

Gated behind a new "Scroll tabs with the mouse wheel" toggle in
Settings > Display (tabBarWheelScroll, default on).
@peterjmorgan peterjmorgan force-pushed the feat/tab-bar-wheel-scroll branch from d61dd74 to 5f237ff Compare June 27, 2026 20:02

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d61dd74b79

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/renderer/stores/settingsStore.ts Outdated
persistentWebLink: false,
webInterfaceUseCustomPort: false,
webInterfaceCustomPort: 8080,
webInterfaceCustomPort: 9080,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the web interface default port at 8080

This unrelated default change makes the renderer start from 9080 while src/main/stores/defaults.ts, src/shared/settingsMetadata.ts, the web-server fallback, the Live overlay placeholder, and existing tests still define 8080. If the renderer default is consumed before or without successful settings hydration, the custom-port UI can show or persist a conflicting default that is unrelated to tab scrolling.

Useful? React with 👍 / 👎.

showFilePreviewsInUnreadFilter: false,
useCmd0AsLastTab: true,
showBrowserTabDomain: true,
tabBarWheelScroll: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Register the tabBarWheelScroll setting everywhere

When this setting is managed outside the Display tab, it is not discoverable or resettable because the new key is added only to the renderer store and UI. The root AGENTS.md says new settings must also be registered in src/shared/settingsMetadata.ts, src/main/stores/defaults.ts, and src/renderer/components/Settings/searchableSettings.ts; the CLI list/reset/get paths rely on SETTINGS_METADATA, and Settings search relies on searchableSettings, so tabBarWheelScroll is omitted from CLI listings, cannot be reset by maestro-cli settings reset tabBarWheelScroll, and searches like mouse wheel do not find the toggle.

Useful? React with 👍 / 👎.

// VS Code convention; trackpads and Shift+wheel emit deltaX, so we follow
// whichever axis the device reports. Attached as a native, non-passive
// listener because React's synthetic onWheel runs passively and cannot call
// preventDefault — without it the wheel would also scroll an outer container.

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 Badge Replace the U+2014 comment character

The root AGENTS.md writing style forbids em dash and en dash characters anywhere, including code comments. This newly added comment contains U+2014, so the patch violates the repo's mandatory style even though the code still runs.

Useful? React with 👍 / 👎.

// preventDefault — without it the wheel would also scroll an outer container.
useEffect(() => {
const el = tabBarRef.current;
if (!el || !tabBarWheelScroll) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Honor the off state for horizontal wheel input

With tabBarWheelScroll off, this effect returns without installing any wheel listener, but the tab bar remains an overflow-x-auto element. Trackpads and Shift+wheel produce horizontal deltaX, so the browser's native horizontal scrolling still moves the tab strip even though the Display toggle says disabling it ignores the wheel over the tab bar; users disabling the feature because wheel gestures interfere still see those inputs move tabs.

Useful? React with 👍 / 👎.

@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 `@src/renderer/stores/settingsStore.ts`:
- Around line 2537-2539: The hydration logic for tabBarWheelScroll in
settingsStore should validate the persisted value’s type before applying it,
since allSettings is a Record<string, unknown> and checking only for undefined
allows non-boolean values like strings to pass through. Update the patch
assignment in the settings hydration path to accept only actual booleans (or
explicitly coerce/ignore invalid values) so useSettings() reloads cannot
preserve a truthy non-boolean and TabBar.tsx reads the setting correctly.
🪄 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

Run ID: fc286072-931f-4b95-9cd8-51fc84a6279d

📥 Commits

Reviewing files that changed from the base of the PR and between d61dd74 and 5f237ff.

📒 Files selected for processing (4)
  • src/renderer/components/Settings/tabs/DisplayTab.tsx
  • src/renderer/components/TabBar/TabBar.tsx
  • src/renderer/hooks/settings/useSettings.ts
  • src/renderer/stores/settingsStore.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/renderer/hooks/settings/useSettings.ts
  • src/renderer/components/Settings/tabs/DisplayTab.tsx
  • src/renderer/components/TabBar/TabBar.tsx

Comment on lines +2537 to +2539
if (allSettings['tabBarWheelScroll'] !== undefined)
patch.tabBarWheelScroll = allSettings['tabBarWheelScroll'] as boolean;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate the persisted type before hydrating tabBarWheelScroll.

Line 2537 reads from Record<string, unknown> but only guards against undefined, so a non-boolean persisted value survives unchanged. Because useSettings() reloads on external settings changes and TabBar.tsx treats this field by truthiness, values like 'false' will still keep wheel scrolling enabled.

Suggested fix
-		if (allSettings['tabBarWheelScroll'] !== undefined)
-			patch.tabBarWheelScroll = allSettings['tabBarWheelScroll'] as boolean;
+		if (typeof allSettings['tabBarWheelScroll'] === 'boolean')
+			patch.tabBarWheelScroll = allSettings['tabBarWheelScroll'];
📝 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
if (allSettings['tabBarWheelScroll'] !== undefined)
patch.tabBarWheelScroll = allSettings['tabBarWheelScroll'] as boolean;
if (typeof allSettings['tabBarWheelScroll'] === 'boolean')
patch.tabBarWheelScroll = allSettings['tabBarWheelScroll'];
🤖 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 `@src/renderer/stores/settingsStore.ts` around lines 2537 - 2539, The hydration
logic for tabBarWheelScroll in settingsStore should validate the persisted
value’s type before applying it, since allSettings is a Record<string, unknown>
and checking only for undefined allows non-boolean values like strings to pass
through. Update the patch assignment in the settings hydration path to accept
only actual booleans (or explicitly coerce/ignore invalid values) so
useSettings() reloads cannot preserve a truthy non-boolean and TabBar.tsx reads
the setting correctly.

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