Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest'

import { forceLoneHeaderForPanes } from './lone-header'
import { forceLoneHeaderForPanes, hasCloseableMainTile, resolveZoneHeaderHidden } from './lone-header'

describe('forceLoneHeaderForPanes', () => {
const chrome =
Expand Down Expand Up @@ -35,3 +35,76 @@ describe('forceLoneHeaderForPanes', () => {
expect(forceLoneHeaderForPanes(['files'], chrome('right'), noCollapse)).toBe(false)
})
})

describe('resolveZoneHeaderHidden', () => {
const mainChrome = () => ({ placement: 'main' as const, uncloseable: false })
const workspaceChrome = () => ({ placement: 'main' as const, uncloseable: true })

it('keeps the strip visible when a sticky hide meets a closeable preview', () => {
const shown = ['workspace', 'preview-tile:url:x']
const chromeOf = (id: string) => (id === 'workspace' ? workspaceChrome() : mainChrome())

expect(
resolveZoneHeaderHidden({
forceLoneHeader: forceLoneHeaderForPanes(shown, chromeOf, () => false),
hasCloseableMainTile: hasCloseableMainTile(shown, chromeOf),
headerHiddenFlag: true,
shownLength: shown.length
})
).toBe(false)
})

it('keeps the strip visible for a lone Browser/preview tile even when sticky-hidden', () => {
const shown = ['preview-tile:url:x']
const chromeOf = mainChrome

expect(
resolveZoneHeaderHidden({
forceLoneHeader: forceLoneHeaderForPanes(shown, chromeOf, () => false),
hasCloseableMainTile: hasCloseableMainTile(shown, chromeOf),
headerHiddenFlag: true,
shownLength: shown.length
})
).toBe(false)
})

it('still honors sticky hide for a tool-only zone', () => {
const shown = ['terminal', 'logs']

expect(
resolveZoneHeaderHidden({
forceLoneHeader: forceLoneHeaderForPanes(shown, () => ({}), id => id === 'terminal' || id === 'logs'),
hasCloseableMainTile: false,
headerHiddenFlag: true,
shownLength: shown.length
})
).toBe(true)
})

it('still auto-hides a lone uncloseable workspace', () => {
const shown = ['workspace']
const chromeOf = workspaceChrome

expect(
resolveZoneHeaderHidden({
forceLoneHeader: forceLoneHeaderForPanes(shown, chromeOf, () => false),
hasCloseableMainTile: hasCloseableMainTile(shown, chromeOf),
shownLength: shown.length
})
).toBe(true)
})

it('still honors headerVeto for full-page views', () => {
const shown = ['preview-tile:url:x']
const chromeOf = mainChrome

expect(
resolveZoneHeaderHidden({
forceLoneHeader: true,
hasCloseableMainTile: hasCloseableMainTile(shown, chromeOf),
headerVeto: true,
shownLength: shown.length
})
).toBe(true)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,55 @@ export interface LoneHeaderChrome {
uncloseable?: boolean
}

/** True when any shown pane is a closeable main-strip tile (preview / session / page). */
export function hasCloseableMainTile(
shown: readonly string[],
chromeOf: (id: string) => LoneHeaderChrome
): boolean {
return shown.some(id => {
const chrome = chromeOf(id)

return !chrome.uncloseable && chrome.placement === 'main'
})
}

export function forceLoneHeaderForPanes(
shown: readonly string[],
chromeOf: (id: string) => LoneHeaderChrome,
isCollapsePane: (id: string) => boolean
): boolean {
// "This pane can be closed, so it must expose the ✕." Only the uncloseable
// workspace is exempt; standing side chrome (files / sessions) isn't 'main'.
if (
shown.some(id => {
const chrome = chromeOf(id)

return !chrome.uncloseable && chrome.placement === 'main'
})
) {
if (hasCloseableMainTile(shown, chromeOf)) {
return true
}

return shown.length === 1 && isCollapsePane(shown[0])
}

/**
* Whether the zone tab strip should stay hidden.
*
* `headerHiddenFlag` is the user's sticky "Hide tab bar" choice. That preference
* still applies to tool-only zones, but it must not win over a closeable main
* tile (in-app Browser / preview / session): otherwise the strip and close control
* disappear with no recovery surface on the body.
*/
export function resolveZoneHeaderHidden(options: {
headerVeto?: boolean
/** `node.headerHidden` — explicit sticky hide, or undefined for auto. */
headerHiddenFlag?: boolean
shownLength: number
forceLoneHeader: boolean
hasCloseableMainTile: boolean
}): boolean {
if (options.headerVeto) {
return true
}

if (options.hasCloseableMainTile) {
return false
}

return options.headerHiddenFlag ?? (options.shownLength <= 1 && !options.forceLoneHeader)
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ import {
} from '../tab-selection'

import { type DoubleTapContext, startPaneDrag } from './drag-session'
import { forceLoneHeaderForPanes } from './lone-header'
import { forceLoneHeaderForPanes, hasCloseableMainTile, resolveZoneHeaderHidden } from './lone-header'
import { useActiveTabVisible } from './tab-strip-scroll'
import { paneChrome } from './track-model'

Expand Down Expand Up @@ -247,11 +247,20 @@ export function TreeGroup({
// always shows its header (it IS the header).
// Session-tile ids force the header even before chrome registers — cycling
// onto a freshly-split tile used to land headerless ("name card missing").
const forceLoneHeader = forceLoneHeaderForPanes(shown, id => paneChrome(paneFor(id)), isCollapsePane)
const chromeOf = (id: string) => paneChrome(paneFor(id))
const forceLoneHeader = forceLoneHeaderForPanes(shown, chromeOf, isCollapsePane)
const closeableMainTile = hasCloseableMainTile(shown, chromeOf)

// A full-page view (headerVeto) suppresses the strip while it's the active
// pane — a page is not a tab-able surface; the bar returns with the chat.
const headerHidden = paneChrome(active).headerVeto || (node.headerHidden ?? (shown.length <= 1 && !forceLoneHeader))
// Sticky "Hide tab bar" must not trap a closeable preview/Browser without ✕.
const headerHidden = resolveZoneHeaderHidden({
forceLoneHeader,
hasCloseableMainTile: closeableMainTile,
headerHiddenFlag: node.headerHidden,
headerVeto: Boolean(paneChrome(active).headerVeto),
shownLength: shown.length
})

// A group collapses ALONG its parent split's axis. In a row that means the
// WIDTH collapses — a full-width horizontal header would strand a tall
Expand Down
Loading