- )
-}
-
export function ContribController() {
const sidebarOpen = useStore($sidebarOpen)
const statusbarVisible = useStore($statusbarVisible)
@@ -864,57 +842,7 @@ export function ContribController() {
data-contrib-shell=""
style={{ '--titlebar-height': '0px' } as CSSProperties}
>
- {/* Title bar: fixed chrome outside the grid, composable via slots.
- Layout contract (no contribution can break it):
- - a full-bar DRAG BASE underneath (pointer-events-none, like
- AppShell's drag strips) — everywhere without content drags
- the window;
- - each slot region is width-fit, no-drag, pointer-events-auto,
- so every contribution is clickable by construction;
- - LEFT/RIGHT slots align to the MAIN PANE's geometry via the
- tree-published --workspace-left/right vars (pure CSS, no rect
- threading), clamped to clear the REAL TitlebarControls
- clusters (fixed, z-70); center is truly window-centered. */}
-
- {/* Drag strips, AppShell-style: cut to AVOID the fixed control
- clusters instead of overlapping them — Electron's no-drag
- carve-out of fixed/transformed elements is unreliable, so a
- full-bar drag base kills their clicks. In-flow slot content
- still carves via its own no-drag wrapper (the same pattern as
- the app's session-title button). */}
-
-
-
-
-
-
-
-
+
{/* "Close running tab?" — the busy/input-blocked tile close gate. */}
diff --git a/apps/desktop/src/app/contrib/wiring.tsx b/apps/desktop/src/app/contrib/wiring.tsx
index 2e7881845e..1a560faa5d 100644
--- a/apps/desktop/src/app/contrib/wiring.tsx
+++ b/apps/desktop/src/app/contrib/wiring.tsx
@@ -1144,20 +1144,12 @@ export function ContribWiring({ children }: { children: ReactNode }) {
}
const titlebarToolsRight = titlebarToolsRightCss(nativeOverlayWidth, titlebarChrome)
- // Pane-registered tools (preview's monitor/devtools cluster) anchor flush
- // against the static system cluster — in the tree layout the titlebar band
- // sits ABOVE the grid, so AppShell's pane-width anchoring doesn't apply.
- // Count every button the static cluster actually renders: four systemTools
- // (layout, haptics, keybinds, settings) PLUS the always-present
- // right-sidebar toggle (see titlebar-controls.tsx). A shared width that
- // under-counts leaves the find bar, the titlebar header padding, and the
- // pane-cluster anchor overlapping the fifth button.
- const SYSTEM_TOOL_COUNT = 5
- const paneToolCount = rightTitlebarTools.filter(tool => !tool.hidden).length
- const systemToolsWidth = titlebarToolsWidthCss(SYSTEM_TOOL_COUNT)
+ // App controls live on the left; flip and the right toggle share the right.
+ const titlebarToolsWidth = titlebarToolsWidthCss(2)
- const titlebarToolsWidth =
- paneToolCount > 0 ? `calc(${systemToolsWidth} + ${titlebarToolsWidthCss(paneToolCount)})` : systemToolsWidth
+ const leftToolsWidth = titlebarToolsWidthCss(
+ 4 + [...leftTitlebarTools, ...rightTitlebarTools].filter(tool => !tool.hidden).length
+ )
return (
@@ -1167,10 +1159,10 @@ export function ContribWiring({ children }: { children: ReactNode }) {
{
'--titlebar-controls-left': `${controlsPos.left}px`,
'--titlebar-controls-top': `${controlsPos.top}px`,
+ '--titlebar-controls-width': leftToolsWidth,
'--titlebar-controls-y-nudge': titlebarControlsYNudge(titlebarChrome),
'--titlebar-tools-right': titlebarToolsRight,
- '--titlebar-tools-width': titlebarToolsWidth,
- '--shell-preview-toolbar-gap': systemToolsWidth
+ '--titlebar-tools-width': titlebarToolsWidth
} as CSSProperties
}
>
diff --git a/apps/desktop/src/app/shell/titlebar-controls.tsx b/apps/desktop/src/app/shell/titlebar-controls.tsx
index 632e7a9bf0..d54d839361 100644
--- a/apps/desktop/src/app/shell/titlebar-controls.tsx
+++ b/apps/desktop/src/app/shell/titlebar-controls.tsx
@@ -8,12 +8,12 @@ import { resetLayoutTree } from '@/components/pane-shell/tree/store'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Tip, TipKeybindLabel } from '@/components/ui/tooltip'
+import { Slot } from '@/contrib/react/slot'
import { useI18n } from '@/i18n'
import { compactNumber } from '@/lib/format'
import { triggerHaptic } from '@/lib/haptics'
import { formatModifierToken } from '@/lib/keybinds/combo'
import { cn } from '@/lib/utils'
-import { $hapticsMuted, toggleHapticsMuted } from '@/store/haptics'
import { toggleHud } from '@/store/hud'
import {
$fileBrowserOpen,
@@ -134,7 +134,6 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
const navigate = useNavigate()
const location = useLocation()
const modHeld = useModifierHeld()
- const hapticsMuted = useStore($hapticsMuted)
const fileBrowserOpen = useStore($fileBrowserOpen)
const panesFlipped = useStore($panesFlipped)
const sidebarOpen = useStore($sidebarOpen)
@@ -142,18 +141,6 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
const unreadBadge = unreadCount > 0 ? unreadCount : undefined
const unreadHint = unreadBadge ? ` · ${t.titlebar.unreadSessions(unreadBadge)}` : ''
- const toggleHaptics = () => {
- if (!hapticsMuted) {
- triggerHaptic('tap')
- }
-
- toggleHapticsMuted()
-
- if (hapticsMuted) {
- window.requestAnimationFrame(() => triggerHaptic('success'))
- }
- }
-
// POSITIONAL toggles: each button shows/hides everything on its physical
// side of the main zone (the layout tree collapses the whole side), so they
// stay correct through flips and rearranges. $sidebarOpen ≙ left side,
@@ -164,30 +151,28 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
const leftLabel = leftEdge.open ? t.titlebar.hideSidebar : t.titlebar.showSidebar
const rightLabel = rightEdge.open ? t.titlebar.hideRightSidebar : t.titlebar.showRightSidebar
- const leftToolbarTools: TitlebarTool[] = [
- {
- actionId: 'view.toggleSidebar',
- badge: panesFlipped ? undefined : unreadBadge,
- icon: ,
- id: 'sidebar',
- label: `${leftLabel}${panesFlipped ? '' : unreadHint}`,
- onSelect: () => {
- triggerHaptic('tap')
- leftEdge.toggle()
- }
- },
- {
- actionId: 'view.flipPanes',
- icon: ,
- id: 'flip-panes',
- label: t.titlebar.swapSidebarSides,
- onSelect: () => {
- triggerHaptic('tap')
- togglePanesFlipped()
- }
- },
- ...leftTools
- ]
+ const sidebarTool: TitlebarTool = {
+ actionId: 'view.toggleSidebar',
+ badge: panesFlipped ? undefined : unreadBadge,
+ icon: ,
+ id: 'sidebar',
+ label: `${leftLabel}${panesFlipped ? '' : unreadHint}`,
+ onSelect: () => {
+ triggerHaptic('tap')
+ leftEdge.toggle()
+ }
+ }
+
+ const flipTool: TitlebarTool = {
+ actionId: 'view.flipPanes',
+ icon: ,
+ id: 'flip-panes',
+ label: t.titlebar.swapSidebarSides,
+ onSelect: () => {
+ triggerHaptic('tap')
+ togglePanesFlipped()
+ }
+ }
const rightSidebarTool: TitlebarTool = {
actionId: 'view.toggleRightSidebar',
@@ -202,8 +187,18 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
tour: 'right-pane-toggle'
}
- // Static system tools — always pinned to the screen's right edge.
+ // App actions stay visible beside the left sidebar toggle.
const systemTools: TitlebarTool[] = [
+ {
+ actionId: 'nav.settings',
+ icon: ,
+ id: 'settings',
+ label: t.titlebar.openSettings,
+ onSelect: () => {
+ triggerHaptic('open')
+ onOpenSettings()
+ }
+ },
{
className: 'group/tool',
// Hover + held ⌘/Ctrl morphs the glyph into its reset form (see
@@ -237,23 +232,6 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
triggerHaptic('open')
toggleHud(hudTargetSessionId())
}
- },
- {
- active: hapticsMuted,
- icon: ,
- id: 'haptics',
- label: hapticsMuted ? t.titlebar.unmuteHaptics : t.titlebar.muteHaptics,
- onSelect: toggleHaptics
- },
- {
- actionId: 'nav.settings',
- icon: ,
- id: 'settings',
- label: t.titlebar.openSettings,
- onSelect: () => {
- triggerHaptic('open')
- onOpenSettings()
- }
}
]
@@ -265,8 +243,7 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
return null
}
- const visibleSystemTools = systemTools.filter(tool => !tool.hidden)
- const visiblePaneTools = tools.filter(tool => !tool.hidden)
+ const visibleLeftTools = [sidebarTool, ...systemTools, ...leftTools, ...tools].filter(tool => !tool.hidden)
return (
<>
@@ -277,42 +254,19 @@ export function TitlebarControls({ leftTools = [], tools = [], onOpenSettings }:
'left-(--titlebar-controls-left) top-(--titlebar-controls-top) translate-y-(--titlebar-controls-y-nudge)'
)}
>
- {leftToolbarTools
- .filter(tool => !tool.hidden)
- .map(tool => (
-
- ))}
+ {visibleLeftTools.map(tool => (
+
+ ))}
+
+
+
- {/*
- Pane-scoped tools (preview's monitor / devtools / refresh / X) render
- as their own fixed cluster. AppShell sets --shell-preview-toolbar-gap
- to either the static cluster's width (file-browser closed → cluster
- sits flush against system tools) or the file-browser pane's width
- (file-browser open → cluster sits flush against the file-browser pane,
- i.e. at the preview pane's right edge). No margin hacks needed.
- */}
- {visiblePaneTools.length > 0 && (
-
- {visiblePaneTools.map(tool => (
-
- ))}
-
- )}
-
- {visibleSystemTools.map(tool => (
-
- ))}
+
>
diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/drag-session.ts b/apps/desktop/src/components/pane-shell/tree/renderer/drag-session.ts
index 594ef3ed53..cfce03016f 100644
--- a/apps/desktop/src/components/pane-shell/tree/renderer/drag-session.ts
+++ b/apps/desktop/src/components/pane-shell/tree/renderer/drag-session.ts
@@ -514,6 +514,20 @@ export function startPaneDrag(
enterZoneMode()
}
+ // A strip is an exact target. Resolve it before the fuzzy zone engine:
+ // near a panel seam, proximity can otherwise pick the neighboring sidebar.
+ const hitStrip = !shift && strips.find(strip => rectContains(strip.rect, x, y))
+
+ if (hitStrip) {
+ return {
+ kind: 'group',
+ groupId: hitStrip.groupId,
+ groupIds: [hitStrip.groupId],
+ pos: 'center',
+ stack: slotBefore(hitStrip.slots, x, moving)
+ }
+ }
+
// The hint updates on highlight-set changes AND on sub-zone position
// changes (center/edge regions within the same primary zone).
const point = { x, y }
diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/index.tsx b/apps/desktop/src/components/pane-shell/tree/renderer/index.tsx
index 3145f563d3..b4f3f7b1a8 100644
--- a/apps/desktop/src/components/pane-shell/tree/renderer/index.tsx
+++ b/apps/desktop/src/components/pane-shell/tree/renderer/index.tsx
@@ -26,6 +26,7 @@ import { type ReactNode, useEffect } from 'react'
import { useLayoutEditHotkey } from '../../edit-mode'
import { publishWorkspaceGeometry } from '../../geometry'
import { $layoutTree, trackActiveTreeGroup } from '../store'
+import { useTabKeyHints } from '../tab-key-hint-state'
import { ZoneEditor } from '../zone-editor'
import { TreeEditBar } from './edit-bar'
@@ -33,10 +34,11 @@ import { FloatingPanes } from './floating-panes'
import { NarrowOverlays } from './narrow-overlays'
import { TreeNode } from './tree-node'
-export function LayoutTreeRoot({ children }: { children?: ReactNode }) {
+export function LayoutTreeRoot({ children, titlebar = false }: { children?: ReactNode; titlebar?: boolean }) {
const tree = useStore($layoutTree)
useLayoutEditHotkey(true)
+ useTabKeyHints()
// Track the interacted zone so ⌘W closes the right tab even when nothing is
// DOM-focused.
useEffect(trackActiveTreeGroup, [])
@@ -72,7 +74,14 @@ export function LayoutTreeRoot({ children }: { children?: ReactNode }) {
display: none;
}
`}
-
+
{/* Non-tiling panes: fixed cards above the tree, outside every zone. */}
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 548e714adf..849efeb5e8 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
@@ -57,6 +57,28 @@ afterEach(() => {
})
describe('TreeGroup', () => {
+ it('keeps a top-edge strip inside its panel and yields native drag while moving a pane', () => {
+ disposePane = registry.register({
+ area: 'panes',
+ data: { placement: 'main' },
+ id: 'terminal',
+ title: 'Terminal',
+ render: () =>
Terminal
+ })
+ vi.stubGlobal('CSS', { escape: (value: string) => value })
+ render()
+ const zone = container!.querySelector('[data-tree-group]')!
+ const strip = zone.querySelector('[data-zone-tabstrip]')!
+ const header = zone.querySelector('[data-panel-header]')!
+ expect(strip.closest('[data-tree-group]')).toBe(zone)
+ expect(header.contains(strip)).toBe(true)
+ expect(zone.querySelectorAll('[data-window-drag-handle]').length).toBeGreaterThan(0)
+ act(() => $treeDragging.set('terminal'))
+ expect(strip.style).toHaveProperty('WebkitAppRegion', 'no-drag')
+ act(() => $treeDragging.set(null))
+ expect(strip.style).toHaveProperty('WebkitAppRegion', '')
+ })
+
it('points the docked-zone chevron in the collapse or restore action direction', () => {
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 234cbdfce7..a582eb8d42 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,6 +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 { ActionsContextMenu, type MenuKit, renderActionItem } from '@/components/ui/actions-menu'
import { Codicon } from '@/components/ui/codicon'
import { DecodeText } from '@/components/ui/decode-text'
@@ -72,6 +73,7 @@ import {
setTreeGroupTabStrip,
treeTabCloseTargets
} from '../store'
+import { TabKeyHint } from '../tab-key-hints'
import {
$tabSelection,
clearTabSelection,
@@ -219,11 +221,17 @@ function ZoneMenu({
export function TreeGroup({
node,
parentAxis,
- railSide = 'left'
+ railSide = 'left',
+ topEdge = false,
+ leftEdge = false,
+ rightEdge = false
}: {
node: GroupNode
parentAxis?: 'column' | 'row'
railSide?: 'left' | 'right'
+ topEdge?: boolean
+ leftEdge?: boolean
+ rightEdge?: boolean
}) {
const { t } = useI18n()
const ref = useRef(null)
@@ -245,7 +253,7 @@ export function TreeGroup({
// overlay, not every zone's header/body (and not the menuDirections walk).
const dragging = useStore($treeDragging)
const editMode = useStore($layoutEditMode)
- const wcOverlap = useWindowControlsOverlap(ref, true)
+ const wcOverlap = useWindowControlsOverlap(ref, !topEdge)
const hiddenPanes = useStore($hiddenTreePanes)
const narrow = useStore($narrowViewport)
@@ -419,6 +427,7 @@ export function TreeGroup({
)}
- {/* Header: the shared pane tab strip (PaneTabStrip + PaneTab). */}
- {headerVisible && (
-
-
- // Drag still moves the pane. Tapping the strip never collapses:
- // the chevron is the collapse affordance. Overloading the header
- // (and, in a lone-tab zone, the tab sitting in it) made a click
- // on the active chip fold the zone — and on a row-docked tile
- // the chip vanished with the body, leaving no mouse path back.
- // A minimized strip still restores on tap (it IS the handle).
- startPaneDrag(
- activeId,
- e,
- node.minimized ? () => restoreTreePane(activeId) : undefined,
- undefined,
- active?.title ?? activeId
- )
- }
- ref={stripRef}
- style={{ cursor: 'grab' }}
- trailing={
- <>
- {minimizable && (
-
- )}
-
- >
- }
- >
- {shown.map(paneId => {
- const isActive = paneId === activeId && !node.minimized
- const chrome = paneChrome(paneFor(paneId))
- const closeable = closeableTab(paneId)
- const title = paneFor(paneId)?.title ?? paneId
- const isSelected = tabSelection?.groupId === node.id && tabSelection.ids.has(paneId)
+ {/* Keep the header INSIDE its zone: titlebar drops use the same panel
+ bounds, strip refs, focus ownership and split geometry as the body. */}
+ {(headerVisible || topEdge) && (
+
+ {topEdge && leftEdge && (
+
+
+
+
+
+
+
+ )}
+ {headerVisible ? (
+
+ {
+ // Native titlebar gaps move the window; tabs keep their own drag.
+ if (!topEdge) {
+ startPaneDrag(
+ activeId,
+ event,
+ node.minimized ? () => restoreTreePane(activeId) : undefined,
+ undefined,
+ active?.title ?? activeId
+ )
+ }
+ }}
+ ref={stripRef}
+ style={{ cursor: 'grab', WebkitAppRegion: dragging ? 'no-drag' : undefined } as CSSProperties}
+ titlebar={topEdge}
+ trailing={
+ <>
+ {minimizable && (
+
+ )}
+
+ >
+ }
+ >
+ {shown.map((paneId, index) => {
+ const isActive = paneId === activeId && !node.minimized
+ const chrome = paneChrome(paneFor(paneId))
+ const closeable = closeableTab(paneId)
+ const title = paneFor(paneId)?.title ?? paneId
+ const isSelected = tabSelection?.groupId === node.id && tabSelection.ids.has(paneId)
- const tab = (
- closeTab(paneId) : undefined}
- onPointerDown={e => {
- // Chrome's tab-selection grammar, ahead of activate/drag:
- // Shift-click ranges from the anchor, ⌥-click (Ctrl-click
- // off-Mac) toggles. Neither activates nor starts a drag —
- // the press IS the selection edit. ⌘-click stays close
- // (PaneTab claims it first) and ⌃-click stays the macOS
- // context menu.
- if (e.button === 0 && e.shiftKey) {
- e.preventDefault()
- e.stopPropagation()
- selectTabRange(node.id, shown, paneId, activeId)
+ const tab = (
+ closeTab(paneId) : undefined}
+ onPointerDown={e => {
+ // Chrome's tab-selection grammar, ahead of activate/drag:
+ // Shift-click ranges from the anchor, ⌥-click (Ctrl-click
+ // off-Mac) toggles. Neither activates nor starts a drag —
+ // the press IS the selection edit. ⌘-click stays close
+ // (PaneTab claims it first) and ⌃-click stays the macOS
+ // context menu.
+ if (e.button === 0 && e.shiftKey) {
+ e.preventDefault()
+ e.stopPropagation()
+ selectTabRange(node.id, shown, paneId, activeId)
- return
- }
+ return
+ }
- if (isToggleSelectClick(e)) {
- e.preventDefault()
- e.stopPropagation()
- toggleTabSelected(node.id, paneId, activeId)
+ if (isToggleSelectClick(e)) {
+ e.preventDefault()
+ e.stopPropagation()
+ toggleTabSelected(node.id, paneId, activeId)
- return
- }
+ return
+ }
- // Tabs ACTIVATE (restoring a collapsed group). Minimize
- // lives on the chevron — overloading the active tab made
- // double-click a minimize/restore/hide lottery. A plain
- // click also collapses any multi-tab selection back to the
- // one tab (Chrome semantics).
- const onTap = () => {
- clearTabSelection()
+ // Tabs ACTIVATE (restoring a collapsed group). Minimize
+ // lives on the chevron — overloading the active tab made
+ // double-click a minimize/restore/hide lottery. A plain
+ // click also collapses any multi-tab selection back to the
+ // one tab (Chrome semantics).
+ const onTap = () => {
+ clearTabSelection()
- if (node.minimized) {
- restoreTreePane(paneId)
- }
+ if (node.minimized) {
+ restoreTreePane(paneId)
+ }
- activateTreePane(node.id, paneId)
- }
+ activateTreePane(node.id, paneId)
+ }
- // Claim the press so the STRIP's own pane-drag handler
- // (parent onPointerDown) can't also fire. startPaneDrag
- // does this internally; the session drag (shared with
- // sidebar rows) doesn't, so do it here for both paths.
- if (e.button === 0) {
- e.preventDefault()
- e.stopPropagation()
- }
+ // Claim the press so the STRIP's own pane-drag handler
+ // (parent onPointerDown) can't also fire. startPaneDrag
+ // does this internally; the session drag (shared with
+ // sidebar rows) doesn't, so do it here for both paths.
+ if (e.button === 0) {
+ e.preventDefault()
+ e.stopPropagation()
+ }
- // Dragging a SELECTED tab carries the whole selection as
- // one block through the generic pane move — a multi-tab
- // drag outranks the pane's own tab drag (the session drop
- // language is single-session).
- const dragSelection = selectionFor(node.id, shown, paneId)
+ // Dragging a SELECTED tab carries the whole selection as
+ // one block through the generic pane move — a multi-tab
+ // drag outranks the pane's own tab drag (the session drop
+ // language is single-session).
+ const dragSelection = selectionFor(node.id, shown, paneId)
- if (dragSelection) {
- startPaneDrag(
- paneId,
- e,
- onTap,
- stripRef.current ? { groupId: node.id, strip: stripRef.current } : undefined,
- t.zones.tabCount(dragSelection.length),
- dragSelection
- )
+ if (dragSelection) {
+ startPaneDrag(
+ paneId,
+ e,
+ onTap,
+ stripRef.current ? { groupId: node.id, strip: stripRef.current } : undefined,
+ t.zones.tabCount(dragSelection.length),
+ dragSelection
+ )
- return
- }
+ return
+ }
- // A pane may own its tab drag (a session tab speaks the
- // session drop language — link/stack/split); `false` defers
- // to the generic pane move (the workspace tab on a fresh
- // draft has no session to link).
- if (!chrome.tabDrag?.(e, onTap)) {
- startPaneDrag(
- paneId,
- e,
- onTap,
- stripRef.current ? { groupId: node.id, strip: stripRef.current } : undefined,
- title
- )
- }
- }}
- role="tab"
- selected={isSelected}
- style={{ cursor: 'grab' }}
- >
- {chrome.tabLead ? (
- {chrome.tabLead()}
- ) : null}
- {tabLabel(paneId)}
-
- )
+ // A pane may own its tab drag (a session tab speaks the
+ // session drop language — link/stack/split); `false` defers
+ // to the generic pane move (the workspace tab on a fresh
+ // draft has no session to link).
+ if (!chrome.tabDrag?.(e, onTap)) {
+ startPaneDrag(
+ paneId,
+ e,
+ onTap,
+ stripRef.current ? { groupId: node.id, strip: stripRef.current } : undefined,
+ title
+ )
+ }
+ }}
+ role="tab"
+ selected={isSelected}
+ style={{ cursor: 'grab' }}
+ >
+ {chrome.tabLead ? (
+
+
+ {chrome.tabLead()}
+
+
+ ) : null}
+ {tabLabel(paneId)}
+
+ )
- // A pane may wrap ITS tab in a domain menu (session verbs on a
- // tile tab); the wrapper needs the key since it's the root.
- return {chrome.tabWrap ? chrome.tabWrap(tab) : tab}
- })}
+ // A pane may wrap ITS tab in a domain menu (session verbs on a
+ // tile tab); the wrapper needs the key since it's the root.
+ return {chrome.tabWrap ? chrome.tabWrap(tab) : tab}
+ })}
- {/* Plain "+" after the last tab — it mints another tab of the kind
+ {/* Plain "+" after the last tab — it mints another tab of the kind
you are LOOKING AT, so a Browser strip makes another Browser
and a chat strip makes another session (mirrors ⌘T, via the
app-registered action). The pointerdown focuses this zone
first, so the tab lands in THIS strip. Hidden when the active
pane is one of a kind and the zone holds no session tabs. */}
- {newTab && !node.minimized && (
- noteActiveTreeGroup(node.id)}
- >
- }
- label={newTab.label}
- onSelect={newTab.onSelect}
- />
-
- )}
-
-
+ {newTab && !node.minimized && (
+ noteActiveTreeGroup(node.id)}
+ >
+ }
+ label={newTab.label}
+ onSelect={newTab.onSelect}
+ />
+
+ )}
+
+
+ ) : (
+
+ )}
+ {topEdge && rightEdge && (
+