diff --git a/apps/desktop/src/app/shell/titlebar.test.ts b/apps/desktop/src/app/shell/titlebar.test.ts
index 3531adb08b..73d7e46e25 100644
--- a/apps/desktop/src/app/shell/titlebar.test.ts
+++ b/apps/desktop/src/app/shell/titlebar.test.ts
@@ -4,6 +4,7 @@ import {
MACOS_TAHOE_DARWIN_MAJOR,
TITLEBAR_CONTROL_OFFSET_X,
TITLEBAR_CONTROL_SIZE,
+ TITLEBAR_DRAG_HANDLE_WIDTH,
TITLEBAR_EDGE_INSET,
TITLEBAR_FALLBACK_WINDOW_BUTTON_X,
TITLEBAR_ICON_SIZE,
@@ -22,6 +23,10 @@ describe('titlebar sizing', () => {
expect(titlebarIconSizeCss()).toBe('13.9px')
})
+ it('reserves a fixed native drag target beside titlebar tabs', () => {
+ expect(TITLEBAR_DRAG_HANDLE_WIDTH).toBe(48)
+ })
+
it('reserves width from abutting hit targets only', () => {
expect(titlebarToolsWidthCss(4)).toBe('calc(4 * var(--titlebar-control-size))')
})
diff --git a/apps/desktop/src/app/shell/titlebar.ts b/apps/desktop/src/app/shell/titlebar.ts
index e23ed4a381..1d58896d59 100644
--- a/apps/desktop/src/app/shell/titlebar.ts
+++ b/apps/desktop/src/app/shell/titlebar.ts
@@ -1,6 +1,8 @@
import type { HermesConnection } from '@/global'
export const TITLEBAR_HEIGHT = 34
+/** Permanent native drag target beside a top-edge tab strip. */
+export const TITLEBAR_DRAG_HANDLE_WIDTH = 48
export const MACOS_TRAFFIC_LIGHTS_HEIGHT = 14
/** Titlebar tool hit target (both axes). */
export const TITLEBAR_CONTROL_SIZE = 24
diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.test.tsx b/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.test.tsx
index 663d771625..2bb454aacc 100644
--- a/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.test.tsx
+++ b/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.test.tsx
@@ -53,10 +53,61 @@ afterEach(() => {
root = null
container = null
disposePane = null
+ globalThis.document.querySelectorAll('[data-titlebar-cluster]').forEach(element => element.remove())
+ vi.restoreAllMocks()
vi.unstubAllGlobals()
})
describe('TreeGroup', () => {
+ it('keeps a fixed native drag handle beside crowded top-edge tabs', () => {
+ const paneIds = ['terminal', 'terminal-2', 'terminal-3', 'terminal-4', 'terminal-5']
+
+ const disposers = paneIds.map(id =>
+ registry.register({
+ area: 'panes',
+ data: { placement: 'main' },
+ id,
+ render: () =>
{id}
,
+ title: id
+ })
+ )
+
+ disposePane = () => disposers.forEach(dispose => dispose())
+ vi.stubGlobal('CSS', { escape: (value: string) => value })
+ vi.stubGlobal(
+ 'ResizeObserver',
+ class {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ }
+ )
+ vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function () {
+ if (this.matches('[data-titlebar-cluster="left"]')) {
+ return { left: 0, right: 100 } as DOMRect
+ }
+
+ if (this.matches('[data-titlebar-cluster="right"]')) {
+ return { left: 700, right: 800 } as DOMRect
+ }
+
+ return { left: 0, right: 800, width: 800 } as DOMRect
+ })
+ const leftControls = globalThis.document.createElement('div')
+ leftControls.dataset.titlebarCluster = 'left'
+ const rightControls = globalThis.document.createElement('div')
+ rightControls.dataset.titlebarCluster = 'right'
+ globalThis.document.body.append(leftControls, rightControls)
+
+ render()
+
+ const handle = container!.querySelector('[data-window-drag-handle]')!
+ expect(container!.querySelectorAll('[data-tree-tab]').length).toBe(paneIds.length)
+ expect(handle).toBeTruthy()
+ expect(handle.style.width).not.toBe('')
+ expect(handle.className).toContain('shrink-0')
+ })
+
it('keeps a top-edge strip inside its panel and yields native drag while moving a pane', () => {
disposePane = registry.register({
area: 'panes',
diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx b/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx
index 469f129df5..43e8811e13 100644
--- a/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx
+++ b/apps/desktop/src/components/pane-shell/tree/renderer/tree-group.tsx
@@ -12,7 +12,7 @@
import { useStore } from '@nanostores/react'
import { type CSSProperties, Fragment, type ReactNode, type RefObject, useEffect, useRef, useState } from 'react'
-import { TITLEBAR_HEIGHT } from '@/app/shell/titlebar'
+import { TITLEBAR_DRAG_HANDLE_WIDTH, TITLEBAR_HEIGHT } from '@/app/shell/titlebar'
import { ActionsContextMenu, type MenuKit, renderActionItem } from '@/components/ui/actions-menu'
import { Codicon } from '@/components/ui/codicon'
import { DecodeText } from '@/components/ui/decode-text'
@@ -696,11 +696,18 @@ export function TreeGroup({
) : null}
- {topEdge && (!headerVisible || tabsBelowControls) && (
+ {topEdge && (
)}
{topEdge && (