diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/strip-visibility.test.ts b/apps/desktop/src/components/pane-shell/tree/renderer/strip-visibility.test.ts index 8485e1dac1..9d745e5c97 100644 --- a/apps/desktop/src/components/pane-shell/tree/renderer/strip-visibility.test.ts +++ b/apps/desktop/src/components/pane-shell/tree/renderer/strip-visibility.test.ts @@ -44,11 +44,16 @@ describe('no dead zone', () => { expect(resolveTabStripVisible({ mode: 'never', shown: [toolPanel()] })).toBe(true) }) + it('keeps the strip for a lone workspace even when the zone says never', () => { + // Workspace cannot leave the tree, but Close still empties it to a draft + // and + still opens a tab. Chromeless is a dead zone for those handles. + expect(resolveTabStripVisible({ mode: 'never', shown: [workspace()] })).toBe(true) + }) + it('still hides a zone that cannot strand anything', () => { - // The workspace is uncloseable, a stack is reachable by tab cycling, and - // hide-only chrome (sessions / Bots) keeps its panes + ⌘⌥T — the invariant - // protects handles, it does not veto hiding as such. - expect(resolveTabStripVisible({ mode: 'never', shown: [workspace()] })).toBe(false) + // A stack is reachable by tab cycling, and hide-only chrome (sessions / + // Bots) keeps its panes + ⌘⌥T — the invariant protects handles, it does + // not veto hiding as such. expect(resolveTabStripVisible({ mode: 'never', shown: [toolPanel(), toolPanel()] })).toBe(false) expect(resolveTabStripVisible({ mode: 'never', shown: [sideChrome()] })).toBe(false) expect(resolveTabStripVisible({ mode: 'never', shown: [sideChrome(), sideChrome()] })).toBe(false) @@ -86,15 +91,13 @@ describe('a full-page view', () => { }) // Every launch and client update lands on a lone workspace. Chromeless, that -// layout had no session switcher and no "+" until the app-wide default was -// flipped to always — the "tab header disappeared" reports (#89350). +// layout had no session switcher, no Close and no "+" (#89350). describe('a lone workspace', () => { - it('keeps its strip on auto, and only on auto', () => { + it('keeps its strip whatever the zone says', () => { expect(resolveTabStripVisible({ shown: [workspace()] })).toBe(true) + expect(resolveTabStripVisible({ mode: 'never', shown: [workspace()] })).toBe(true) // Standing side chrome never joins the tabbed workflow. expect(resolveTabStripVisible({ shown: [sideChrome()] })).toBe(false) - // Hide tabs (menu row / ⌘⌥T) still wins — the workspace strands nothing. - expect(resolveTabStripVisible({ mode: 'never', shown: [workspace()] })).toBe(false) }) }) @@ -127,7 +130,7 @@ describe('tabStripVisibleForZone', () => { it('reads placement, uncloseable and collapse off the contributions', () => { expect(visible(['workspace'])).toBe(true) - expect(visible(['workspace'], 'never')).toBe(false) + expect(visible(['workspace'], 'never')).toBe(true) expect(visible(['tile:a'], 'never')).toBe(true) expect(visible(['terminal'], 'never')).toBe(true) }) @@ -137,15 +140,15 @@ describe('tabStripVisibleForZone', () => { expect(visible(['workspace'])).toBe(true) setTabStripDefault('never') - expect(visible(['workspace'])).toBe(false) + expect(visible(['workspace'])).toBe(true) expect(visible(['workspace', 'terminal'])).toBe(false) }) it("lets a zone's own choice beat the app default", () => { setTabStripDefault('never') - expect(visible(['workspace'], 'always')).toBe(true) + expect(visible(['workspace', 'terminal'], 'always')).toBe(true) setTabStripDefault('always') - expect(visible(['workspace'], 'never')).toBe(false) + expect(visible(['workspace', 'terminal'], 'never')).toBe(false) }) }) diff --git a/apps/desktop/src/components/pane-shell/tree/renderer/strip-visibility.ts b/apps/desktop/src/components/pane-shell/tree/renderer/strip-visibility.ts index d1bc7b4569..908290b0f4 100644 --- a/apps/desktop/src/components/pane-shell/tree/renderer/strip-visibility.ts +++ b/apps/desktop/src/components/pane-shell/tree/renderer/strip-visibility.ts @@ -38,12 +38,13 @@ export interface StripZone { /** * A pane is STRANDED without a strip when the strip is the only thing carrying - * its handle: a lone closeable tile needs its ✕, a lone tool panel needs a chip - * to grab. The uncloseable workspace is not strandable — it cannot be closed - * or lost, so an explicit `never` may hide its strip. Hide-only chrome (sessions - * / Bots) is the same: the panes stay, Show/Hide is a separate verb, and a - * hidden strip comes back via ⌘⌥T. Treating it as stranded at any count made - * Hide tabs a silent no-op on the sessions sidebar. + * its handle: a lone main tile needs its ✕ and +, a lone tool panel needs a + * chip to grab. The workspace cannot leave the tree, but Close still empties + * it to a draft and + still opens a tab — chromeless is a dead zone for those + * handles. Hide-only chrome (sessions / Bots) is different: the panes stay, + * Show/Hide is a separate verb, and a hidden strip comes back via ⌘⌥T. + * Treating side chrome as stranded at any count made Hide tabs a silent no-op + * on the sessions sidebar. * * This outranks an explicit `never` on purpose. "Hide the strip" is a request * about chrome, never a request to make a surface unreachable, and a zone that @@ -64,7 +65,7 @@ function stranded(shown: readonly StripPane[]): boolean { const [only] = shown - return only.collapsePane || (!only.uncloseable && only.placement === 'main') + return only.collapsePane || only.placement === 'main' } export function resolveTabStripVisible(zone: StripZone): boolean { @@ -87,17 +88,10 @@ export function resolveTabStripVisible(zone: StripZone): boolean { return zone.mode === 'always' } - // Auto: two or more panes need a strip to switch between them. - if (zone.shown.length > 1) { - return true - } - - // A lone MAIN tile keeps its strip too. The strip is the session switcher: - // its tab names the chat and its "+" opens the next one, so a chromeless - // lone workspace — the layout every launch and update lands on — read as - // "my tabs disappeared". Standing side chrome still goes without; hiding - // the workspace's strip remains an explicit choice (`never`, ⌘⌥T). - return zone.shown.some(pane => pane.placement === 'main') + // Auto: two or more panes need a strip to switch between them. A lone main + // tile never reaches here (it is stranded above), so what is left alone is + // standing side chrome, which goes without. + return zone.shown.length > 1 } /**