Skip to content

Conversation

@devin-ai-integration
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot commented Jan 4, 2026

feat: implement drag-to-pop-out functionality for tabs

Summary

This PR adds the ability to drag tabs out of the window to create a new pop-out window. It integrates the tauri-plugin-drag-as-window from CrabNebula's drag-rs library.

Changes:

  • Added tauri-plugin-drag-as-window Rust dependency and registered the plugin
  • Added @crabnebula/tauri-plugin-drag-as-window npm package
  • Created DraggableReorderItem component that wraps Reorder.Item with drag direction detection
  • Added tabToInput helper to convert Tab state to TabInput for serialization
  • Added required Tauri permissions (drag-as-window:default, core:webview:allow-create-webview-window)

Updates since last revision

  • Added dedicated Popout window type: Instead of reusing the Main window type, added a new Popout(String) variant to the AppWindow enum in Rust with proper Display, FromStr, and WindowImpl implementations
  • Created /app/popout route: New dedicated route for popout windows with its own layout (_layout.tsx) and index (_layout.index.tsx) that provides necessary context providers
  • Created PopoutContent component: New component (components/popout/content.tsx) that renders tab content in a simplified popout UI with traffic lights and a title header
  • Updated pop-out URL: Changed from /app/main?popout=true&tab=... to /app/popout?tab=...
  • Regenerated TypeScript bindings: Updated bindings.gen.ts to include the new Popout type

Previous fixes: Drag preview styling with data-tab-pill attribute; Alt+click pop-out trigger; data-tauri-drag-region="false" attribute; pointer capture for reliable move events

Review & Testing Checklist for Human

  • CRITICAL: Verify pop-out window loads at /app/popout - The new window should load without 404 and render the tab content correctly
  • CRITICAL: Verify context providers work in popout - AI chat, search, and other features that depend on context should function in the popout window
  • Test drag preview styling - When dragging a tab, the preview should have rounded corners and properly aligned text
  • Test horizontal tab reordering still works - Drag tabs left/right within the tab bar
  • Review unused code - apps/desktop/src/hooks/useDragAsWindow.ts appears to be unused dead code

Recommended test plan:

  1. Run ONBOARDING=0 pnpm -F desktop tauri dev
  2. Open multiple tabs (note, calendar, settings, etc.)
  3. Drag a tab vertically downward out of the tab bar
  4. Verify the pop-out window opens at /app/popout?tab=... without errors
  5. Verify the popout window renders the correct tab content with a title header
  6. Test horizontal tab reordering still works (drag tabs left/right)

Notes

  • CI test failures are pre-existing - The failing tests in persister.test.ts also fail on the main branch and are unrelated to this PR
  • User confirmed drag-to-pop-out is now working on macOS (without Alt+click)
  • Drag threshold is 15px with direction detection (horizontal vs vertical movement)
  • The Popout window type uses a unique string ID in the format popout-{tab.type}-{timestamp}
  • Requested by: @yujonglee ([email protected])
  • Link to Devin run: https://app.devin.ai/sessions/45a4647001694f7c9fc7c9bb48931380

- Add tauri-plugin-drag-as-window dependency from drag-rs library
- Register drag-as-window plugin in Tauri app initialization
- Add drag-as-window:default and webview permissions
- Add @crabnebula/tauri-plugin-drag-as-window npm package
- Create DraggableTabItem component for drag-to-pop-out behavior
- Create useDragAsWindow hook for tab pop-out functionality
- Add tabToInput helper function to convert Tab to TabInput
- Integrate drag-as-window with tab bar in Header component

Users can now drag tabs vertically out of the window to create
a new pop-out window containing that tab's content.

Co-Authored-By: yujonglee <[email protected]>
@devin-ai-integration
Copy link
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR that start with 'DevinAI' or '@devin'.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

@netlify
Copy link

netlify bot commented Jan 4, 2026

Deploy Preview for hyprnote canceled.

Name Link
🔨 Latest commit db852bf
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote/deploys/695a6b50eb582400084dad3a

@netlify
Copy link

netlify bot commented Jan 4, 2026

Deploy Preview for howto-fix-macos-audio-selection canceled.

Name Link
🔨 Latest commit db852bf
🔍 Latest deploy log https://app.netlify.com/projects/howto-fix-macos-audio-selection/deploys/695a6b5031b486000858cf4b

@netlify
Copy link

netlify bot commented Jan 4, 2026

Deploy Preview for hyprnote-storybook canceled.

Name Link
🔨 Latest commit db852bf
🔍 Latest deploy log https://app.netlify.com/projects/hyprnote-storybook/deploys/695a6b505d489b0008141198

devin-ai-integration bot and others added 7 commits January 4, 2026 12:14
…pture

- Create DraggableReorderItem component that integrates with Reorder.Item
- Use useDragControls to manually control when reorder drag starts
- Set dragListener={false} on Reorder.Item to prevent automatic pointer capture
- Detect drag direction: horizontal starts reorder, vertical starts pop-out
- Add proper error handling with .catch() and .finally() for dragAsWindow
- Lower threshold to 15px for more responsive drag detection

Co-Authored-By: yujonglee <[email protected]>
- Add WebkitAppRegion: 'no-drag' style to prevent Tauri's drag region from blocking pointer events on macOS
- Add setPointerCapture in onPointerDown to ensure we receive move events even when pointer leaves element
- Store native pointerdown event and use it for dragControls.start() as Framer Motion expects
- Release pointer capture before starting either Framer Motion drag or native dragAsWindow
- Update handlePointerUp and handlePointerCancel to accept PointerEvent and release capture

Co-Authored-By: yujonglee <[email protected]>
- Add data-tauri-drag-region='false' attribute for explicit Tauri opt-out on macOS
- Guard all releasePointerCapture calls with hasPointerCapture to prevent exceptions
- Remove inline comments following code style conventions

Co-Authored-By: yujonglee <[email protected]>
macOS native drag APIs may require the drag to be initiated from the
initial user gesture (pointerdown) rather than from pointermove.

This adds an alternative approach: hold Alt/Option while clicking a tab
to immediately trigger the pop-out drag. This tests whether calling
dragAsWindow from pointerdown fixes the macOS issue.

The vertical drag detection from pointermove is still available as a
fallback for platforms where it works.

Co-Authored-By: yujonglee <[email protected]>
1. Drag preview styling: Use data-tab-pill attribute to find the styled
   tab element (with rounded corners, background, etc.) instead of
   capturing the plain wrapper element. This makes the drag preview
   look correct with rounded corners and proper text alignment.

2. Pop-out URL routing: Change the pop-out window URL from / to /app/main
   so it matches a valid route in the TanStack Router configuration.
   This fixes the 404 error when opening pop-out windows.

Co-Authored-By: yujonglee <[email protected]>
Comment on lines +178 to +188
const handlePointerUp = useCallback((e: React.PointerEvent) => {
if (
containerRef.current &&
containerRef.current.hasPointerCapture(e.pointerId)
) {
containerRef.current.releasePointerCapture(e.pointerId);
}
dragStartPosRef.current = null;
hasStartedReorderRef.current = false;
pointerDownEventRef.current = null;
}, []);
Copy link
Contributor

Choose a reason for hiding this comment

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

Missing isDraggingRef reset in handlePointerUp causes drag functionality to break. If a user initiates a vertical drag (which sets isDraggingRef.current = true on line 125), then releases the pointer before the async dragAsWindow operation completes, the ref remains true. This blocks all subsequent pointer move handling due to the early return on line 100, preventing any further drag operations until the async operation eventually completes.

Fix: Add isDraggingRef.current = false; in the handlePointerUp callback:

const handlePointerUp = useCallback((e: React.PointerEvent) => {
  if (
    containerRef.current &&
    containerRef.current.hasPointerCapture(e.pointerId)
  ) {
    containerRef.current.releasePointerCapture(e.pointerId);
  }
  isDraggingRef.current = false;  // Add this line
  dragStartPosRef.current = null;
  hasStartedReorderRef.current = false;
  pointerDownEventRef.current = null;
}, []);
Suggested change
const handlePointerUp = useCallback((e: React.PointerEvent) => {
if (
containerRef.current &&
containerRef.current.hasPointerCapture(e.pointerId)
) {
containerRef.current.releasePointerCapture(e.pointerId);
}
dragStartPosRef.current = null;
hasStartedReorderRef.current = false;
pointerDownEventRef.current = null;
}, []);
const handlePointerUp = useCallback((e: React.PointerEvent) => {
if (
containerRef.current &&
containerRef.current.hasPointerCapture(e.pointerId)
) {
containerRef.current.releasePointerCapture(e.pointerId);
}
isDraggingRef.current = false;
dragStartPosRef.current = null;
hasStartedReorderRef.current = false;
pointerDownEventRef.current = null;
}, []);

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

- Add Popout(String) variant to AppWindow enum in Rust
- Update Display, FromStr, and WindowImpl implementations for Popout
- Create /app/popout route with layout and index components
- Create PopoutContent component to render tab content in popout windows
- Update drag-to-pop-out code to use /app/popout route instead of /app/main
- Regenerate TypeScript bindings with new Popout type

Co-Authored-By: yujonglee <[email protected]>
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.

2 participants