Skip to content

fix: reliable window drag region in tab bar#69

Merged
matt1398 merged 2 commits intomatt1398:mainfrom
proxikal:fix/window-drag-region
Feb 23, 2026
Merged

fix: reliable window drag region in tab bar#69
matt1398 merged 2 commits intomatt1398:mainfrom
proxikal:fix/window-drag-region

Conversation

@proxikal
Copy link
Contributor

@proxikal proxikal commented Feb 23, 2026

Problem

On macOS, dragging the window by the tab bar was unreliable — roughly a 20% success rate. The drag region was only applied when the sidebar was collapsed and the pane was leftmost, meaning any other state left users with no draggable area. After two days of testing this was confirmed fully resolved on macOS.

On Windows, tab items were not marked no-drag, interfering with tab reordering via DnD.

Changes

Window drag fix (TabBar.tsx, SortableTab.tsx)

  • Apply WebkitAppRegion: drag to the leftmost pane's tab bar regardless of sidebar state — previously gated behind sidebarCollapsed
  • Cap the tab list at 75% width so the drag spacer always has room to the right
  • Add an explicit flex-1 drag spacer between the tab list and action buttons, giving a reliable drag target no matter how many tabs are open
  • Set WebkitAppRegion: no-drag on SortableTab so tab DnD reordering doesn't conflict

Auto-scroll improvements (useAutoScrollBottom.ts, ChatHistory.tsx)

  • Make auto-scroll StrictMode-safe: replace single requestAnimationFrame with double-RAF + cleanup (cancelAnimationFrame on both IDs) to prevent React StrictMode's double-invoke from firing the scroll twice
  • Add needsInitialScrollRef to force scroll-to-bottom on first load after a tab/session switch, even if wasAtBottomBeforeUpdateRef hasn't settled yet
  • Add floating scroll-to-bottom button that appears when the user scrolls up, with correct position offset when the context panel is visible

Testing

  • macOS: drag success rate was ~20% before, confirmed 100% reliable after 2 days of testing
  • Windows: tab DnD reordering unaffected by drag region changes

Summary by CodeRabbit

  • New Features

    • Added a scroll-to-bottom button in chat that appears when you're scrolled up and takes you to the latest messages.
    • Improved initial auto-scroll behavior so conversations reliably jump to bottom after resets.
  • UI Improvements

    • Refined auto-scroll sensitivity for smoother behavior as new messages arrive.
    • Enhanced tab/window drag regions and added a dedicated drag spacer for more consistent dragging.

Includes scroll improvements:
- Scroll to bottom on session open and live auto-scroll
- Make auto-scroll StrictMode-safe via needsInitialScrollRef
- Add floating scroll-to-bottom button in chat view

Window drag fix:
- Apply drag region to leftmost pane TabBar regardless of sidebar state
- Cap tab list at 75% so drag spacer always has room
- Add explicit flex-1 drag spacer between tabs and action buttons
- Set WebkitAppRegion: no-drag on tab items for reordering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gemini-code-assist
Copy link

Summary of Changes

Hello @proxikal, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the user experience by addressing two key areas: window dragging and chat auto-scrolling. It resolves an issue where window dragging on macOS was unreliable and ensures that tab reordering works correctly on Windows. Additionally, it refines the auto-scroll behavior in chat, making it more robust and introducing a convenient scroll-to-bottom button for improved navigation.

Highlights

  • Improved Window Dragging Reliability: The window drag region on macOS has been made consistently available on the leftmost pane's tab bar, regardless of the sidebar's collapsed state. A dedicated drag spacer was also added to ensure a reliable drag target.
  • Enabled Tab Reordering on Windows: Individual tab items are now explicitly marked as 'no-drag' to prevent conflicts with tab reordering via drag-and-drop on Windows.
  • Enhanced Auto-Scroll Logic: The auto-scroll hook was updated to be React StrictMode-safe using a double requestAnimationFrame pattern and now forces an initial scroll-to-bottom on tab or session switches.
  • Added Scroll-to-Bottom Button: A new floating scroll-to-bottom button appears in the chat history when the user scrolls up, providing a quick way to navigate to the latest messages, with correct positioning when the context panel is visible.
Changelog
  • src/renderer/components/chat/ChatHistory.tsx
    • Imported isNearBottom utility and ChevronsDown icon.
    • Implemented state and a callback function (checkScrollButton) to manage the visibility of a scroll-to-bottom button.
    • Updated the useAutoScrollBottom hook's threshold to 300 and destructured scrollToBottom for external use.
    • Added an onScroll event handler to the chat container to trigger checkScrollButton.
    • Rendered a conditional floating scroll-to-bottom button with dynamic positioning based on context panel visibility.
  • src/renderer/components/layout/SortableTab.tsx
    • Applied WebkitAppRegion: no-drag to the SortableTab component's style to prevent interference with window dragging.
  • src/renderer/components/layout/TabBar.tsx
    • Modified the WebkitAppRegion application logic for the tab bar, removing the sidebarCollapsed condition to ensure consistent window dragging.
    • Constrained the tab list's maxWidth to 75% and added the shrink class to allow space for the drag spacer.
    • Introduced a new div element with flex-1 and WebkitAppRegion: drag to serve as a reliable drag spacer between the tab list and action buttons.
  • src/renderer/hooks/useAutoScrollBottom.ts
    • Added needsInitialScrollRef to track whether an initial scroll is required after a resetKey change.
    • Updated the useEffect hook that responds to resetKey changes to set needsInitialScrollRef to true.
    • Refactored the main auto-scroll useEffect to use a double requestAnimationFrame pattern with cleanup for React StrictMode compatibility.
    • Modified the auto-scroll condition within the requestAnimationFrame callback to include needsInitialScrollRef for forced initial scrolls.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@coderabbitai coderabbitai bot added bug Something isn't working feature request New feature or request labels Feb 23, 2026
Copy link

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

Choose a reason for hiding this comment

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

Code Review

The pull request successfully addresses the unreliable window drag regions on macOS and improves the auto-scroll behavior in the chat history. The introduction of a drag spacer in the tab bar and the use of double-requestAnimationFrame for auto-scrolling are solid improvements. I have a few suggestions regarding maintainability (using constants for magic numbers) and ensuring consistency in the drag region logic across different pane states.

Comment on lines +348 to +355
const [showScrollButton, setShowScrollButton] = useState(false);

const checkScrollButton = useCallback(() => {
const container = scrollContainerRef.current;
if (!container) return;
const { scrollTop, scrollHeight, clientHeight } = container;
setShowScrollButton(!isNearBottom(scrollTop, scrollHeight, clientHeight, 300));
}, []);

Choose a reason for hiding this comment

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

medium

The scroll threshold 300 is used in multiple places within this component. It is recommended to define this as a constant (e.g., SCROLL_THRESHOLD) to ensure consistency and make it easier to maintain.

Suggested change
const [showScrollButton, setShowScrollButton] = useState(false);
const checkScrollButton = useCallback(() => {
const container = scrollContainerRef.current;
if (!container) return;
const { scrollTop, scrollHeight, clientHeight } = container;
setShowScrollButton(!isNearBottom(scrollTop, scrollHeight, clientHeight, 300));
}, []);
const SCROLL_THRESHOLD = 300;
// Scroll-to-bottom button visibility
const [showScrollButton, setShowScrollButton] = useState(false);
const checkScrollButton = useCallback(() => {
const container = scrollContainerRef.current;
if (!container) return;
const { scrollTop, scrollHeight, clientHeight } = container;
setShowScrollButton(!isNearBottom(scrollTop, scrollHeight, clientHeight, SCROLL_THRESHOLD));
}, []);

useAutoScrollBottom([conversation], {
threshold: 150,
const { scrollToBottom } = useAutoScrollBottom([conversation], {
threshold: 300,

Choose a reason for hiding this comment

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

medium

Use the constant here as well to keep the threshold in sync with the scroll button visibility logic.

Suggested change
threshold: 300,
threshold: SCROLL_THRESHOLD,

style={{
right:
isContextPanelVisible && allContextInjections.length > 0
? 'calc(320px + 1rem)'

Choose a reason for hiding this comment

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

medium

The 320px value matches the width of the context panel (w-80). It would be safer to use a constant for the panel width to avoid layout issues if the sidebar width is ever adjusted.

Gives users a reliable window-drag target regardless of how many tabs are open. */}
<div
className="flex-1 self-stretch"
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}

Choose a reason for hiding this comment

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

medium

To maintain consistency with the background drag logic defined at line 271, the spacer's drag region should also be conditional on isLeftmostPane. This ensures that only the primary title bar area acts as a window drag handle, preventing unexpected behavior in split-pane layouts.

Suggested change
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
style={{ WebkitAppRegion: isElectronMode() && isLeftmostPane ? 'drag' : undefined } as React.CSSProperties}

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

No actionable comments were generated in the recent review. 🎉


📝 Walkthrough

Walkthrough

Adds a bottom-scroll button to the chat history with scroll visibility detection and a larger auto-scroll threshold (150→300). Updates auto-scroll hook to track resetKey and use a double requestAnimationFrame flow. Adjusts Electron drag-region styling and adds a drag spacer in the tab bar.

Changes

Cohort / File(s) Summary
Chat History & Auto-scroll
src/renderer/components/chat/ChatHistory.tsx, src/renderer/hooks/useAutoScrollBottom.ts
Adds a bottom-scroll button (uses ChevronsDown) with show/hide logic and scroll event wiring; increases auto-scroll threshold from 150 to 300; exposes scrollToBottom via useAutoScrollBottom with externalRef/resetKey integration; implements needsInitialScrollRef and a double RAF flow with cleanup to ensure correct post-update scrolling.
Tab UI & Drag Regions
src/renderer/components/layout/SortableTab.tsx, src/renderer/components/layout/TabBar.tsx
Applies WebkitAppRegion styles to control drag/no-drag areas (SortableTab), removes explicit style type annotation, changes TabBar to always apply leftmost-pane drag region when in Electron mode, constrains tab list width (max 75%), and inserts a drag spacer element between tabs and right-side actions.

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

Copy link

@coderabbitai coderabbitai bot left a comment

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/chat/ChatHistory.tsx (1)

1-10: Reorder imports to follow external → alias → relative grouping.
This keeps the file aligned with the project's import-order convention.

♻️ Proposed import order
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-
-import { isNearBottom, useAutoScrollBottom } from '@renderer/hooks/useAutoScrollBottom';
-import { useTabNavigationController } from '@renderer/hooks/useTabNavigationController';
-import { useTabUI } from '@renderer/hooks/useTabUI';
-import { useVisibleAIGroup } from '@renderer/hooks/useVisibleAIGroup';
-import { useStore } from '@renderer/store';
-import { useVirtualizer } from '@tanstack/react-virtual';
-import { ChevronsDown } from 'lucide-react';
-import { useShallow } from 'zustand/react/shallow';
-
-import { SessionContextPanel } from './SessionContextPanel/index';
-import { ChatHistoryEmptyState } from './ChatHistoryEmptyState';
-import { ChatHistoryItem } from './ChatHistoryItem';
-import { ChatHistoryLoadingState } from './ChatHistoryLoadingState';
-
-import type { ContextInjection } from '@renderer/types/contextInjection';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { useVirtualizer } from '@tanstack/react-virtual';
+import { ChevronsDown } from 'lucide-react';
+import { useShallow } from 'zustand/react/shallow';
+
+import { isNearBottom, useAutoScrollBottom } from '@renderer/hooks/useAutoScrollBottom';
+import { useTabNavigationController } from '@renderer/hooks/useTabNavigationController';
+import { useTabUI } from '@renderer/hooks/useTabUI';
+import { useVisibleAIGroup } from '@renderer/hooks/useVisibleAIGroup';
+import { useStore } from '@renderer/store';
+import type { ContextInjection } from '@renderer/types/contextInjection';
+
+import { SessionContextPanel } from './SessionContextPanel/index';
+import { ChatHistoryEmptyState } from './ChatHistoryEmptyState';
+import { ChatHistoryItem } from './ChatHistoryItem';
+import { ChatHistoryLoadingState } from './ChatHistoryLoadingState';

As per coding guidelines "Organize imports in order: external packages, path aliases (@main, @renderer, @shared, @preload), then relative imports".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/chat/ChatHistory.tsx` around lines 1 - 10, The
imports in ChatHistory.tsx are not grouped per project convention; reorder them
so external packages come first (react, `@tanstack/react-virtual`, lucide-react,
zustand), then path aliases (`@renderer/hooks`, `@renderer/store`), then any
relative imports, preserving existing imported symbols (e.g.,
useCallback/useEffect/useMemo/useRef/useState, useVirtualizer, ChevronsDown,
useShallow, isNearBottom, useAutoScrollBottom, useTabNavigationController,
useTabUI, useVisibleAIGroup, useStore) and add single blank lines between each
group to match the external → alias → relative ordering rule.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/renderer/components/chat/ChatHistory.tsx`:
- Around line 1-10: The imports in ChatHistory.tsx are not grouped per project
convention; reorder them so external packages come first (react,
`@tanstack/react-virtual`, lucide-react, zustand), then path aliases
(`@renderer/hooks`, `@renderer/store`), then any relative imports, preserving
existing imported symbols (e.g., useCallback/useEffect/useMemo/useRef/useState,
useVirtualizer, ChevronsDown, useShallow, isNearBottom, useAutoScrollBottom,
useTabNavigationController, useTabUI, useVisibleAIGroup, useStore) and add
single blank lines between each group to match the external → alias → relative
ordering rule.

- Extract SCROLL_THRESHOLD (300px) constant so auto-scroll hook and
  scroll-button visibility logic stay in sync
- Extract CONTEXT_PANEL_WIDTH_PX (320px) constant to avoid layout drift
  if the context panel width is ever adjusted
- Gate drag spacer on isElectronMode() && isLeftmostPane to match the
  TabBar drag region logic and prevent unintended drag regions in
  split-pane layouts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@matt1398 matt1398 merged commit 9629c0a into matt1398:main Feb 23, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working feature request New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants