fix(desktop): keep the tab strip on a lone workspace in auto mode
Every launch lands on a lone workspace chat, and auto mode hid its strip, so the session tab and its "+" were gone until the app-wide default was set to always. A lone main tile now keeps its strip on auto; Hide tabs and an app-wide "never" still hide it. The siblingMainZone input and the $mainTileZoneCount store it fed are no longer needed and are removed.
This commit is contained in:
@@ -40,7 +40,7 @@ afterEach(() => disposers.splice(0).forEach(dispose => dispose()))
|
||||
const zoneOf = (paneId: string) => findGroupOfPane($layoutTree.get()!, paneId)!
|
||||
|
||||
describe('dragging a session tab into its own zone', () => {
|
||||
it('leaves BOTH chat zones with a strip, and main chromeless again once the tile is gone', () => {
|
||||
it('leaves BOTH chat zones with a strip, and main keeps its own once the tile is gone', () => {
|
||||
expect(tabStripVisibleForGroup(zoneOf('workspace'))).toBe(true)
|
||||
|
||||
moveTreePane('session-tile:b', { groupId: 'grp-main', pos: 'right' })
|
||||
@@ -53,6 +53,7 @@ describe('dragging a session tab into its own zone', () => {
|
||||
|
||||
removeTreePane('session-tile:b')
|
||||
|
||||
expect(tabStripVisibleForGroup(zoneOf('workspace'))).toBe(false)
|
||||
// A lone workspace is the session switcher's home: tab and "+" stay (#89350).
|
||||
expect(tabStripVisibleForGroup(zoneOf('workspace'))).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -11,8 +11,8 @@ const toolPanel = (): StripPane => ({ collapsePane: true, placement: 'bottom' })
|
||||
const sideChrome = (): StripPane => ({ collapsePane: false, placement: 'right' })
|
||||
|
||||
describe('auto (no stored choice)', () => {
|
||||
it('gives a lone workspace no strip and a stack of two a strip', () => {
|
||||
expect(resolveTabStripVisible({ shown: [workspace()] })).toBe(false)
|
||||
it('gives a lone workspace its strip so the tab and + stay on screen', () => {
|
||||
expect(resolveTabStripVisible({ shown: [workspace()] })).toBe(true)
|
||||
expect(resolveTabStripVisible({ shown: [workspace(), sideChrome()] })).toBe(true)
|
||||
})
|
||||
|
||||
@@ -85,19 +85,16 @@ describe('a full-page view', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Dragging a session out of the chat strip into its own zone left main a lone
|
||||
// workspace: the tile kept its tab (stranded), main lost its tab and its "+",
|
||||
// and the two chats side by side read as "the tabs disappeared". Tiles are a
|
||||
// tabbed workflow — a main zone keeps its strip while another main zone exists.
|
||||
describe('a lone main tile beside another main zone', () => {
|
||||
// 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).
|
||||
describe('a lone workspace', () => {
|
||||
it('keeps its strip on auto, and only on auto', () => {
|
||||
expect(resolveTabStripVisible({ shown: [workspace()], siblingMainZone: true })).toBe(true)
|
||||
// The whole-window chat is still chromeless, and standing side chrome
|
||||
// never joins the tabbed workflow.
|
||||
expect(resolveTabStripVisible({ shown: [workspace()], siblingMainZone: false })).toBe(false)
|
||||
expect(resolveTabStripVisible({ shown: [sideChrome()], siblingMainZone: true })).toBe(false)
|
||||
expect(resolveTabStripVisible({ 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()], siblingMainZone: true })).toBe(false)
|
||||
expect(resolveTabStripVisible({ mode: 'never', shown: [workspace()] })).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -123,14 +120,14 @@ describe('tabStripVisibleForZone', () => {
|
||||
isCollapsePane: id => id === 'terminal',
|
||||
mode,
|
||||
paneFor: id => contributions[id],
|
||||
shown,
|
||||
siblingMainZone: false
|
||||
shown
|
||||
})
|
||||
|
||||
afterEach(() => setTabStripDefault('auto'))
|
||||
|
||||
it('reads placement, uncloseable and collapse off the contributions', () => {
|
||||
expect(visible(['workspace'])).toBe(false)
|
||||
expect(visible(['workspace'])).toBe(true)
|
||||
expect(visible(['workspace'], 'never')).toBe(false)
|
||||
expect(visible(['tile:a'], 'never')).toBe(true)
|
||||
expect(visible(['terminal'], 'never')).toBe(true)
|
||||
})
|
||||
@@ -140,6 +137,7 @@ describe('tabStripVisibleForZone', () => {
|
||||
expect(visible(['workspace'])).toBe(true)
|
||||
|
||||
setTabStripDefault('never')
|
||||
expect(visible(['workspace'])).toBe(false)
|
||||
expect(visible(['workspace', 'terminal'])).toBe(false)
|
||||
})
|
||||
|
||||
|
||||
@@ -34,16 +34,13 @@ export interface StripZone {
|
||||
/** Panes currently rendered as chips — chrome-hidden and narrow-collapsed
|
||||
* panes are already filtered out. */
|
||||
shown: readonly StripPane[]
|
||||
/** ANOTHER zone in the layout also hosts a main tile (a chat, a page, a
|
||||
* preview). Auto reads it as "the user is working in tiles". */
|
||||
siblingMainZone?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 a lone chat is free to be chromeless. Hide-only chrome (sessions
|
||||
* 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.
|
||||
@@ -90,19 +87,17 @@ export function resolveTabStripVisible(zone: StripZone): boolean {
|
||||
return zone.mode === 'always'
|
||||
}
|
||||
|
||||
// Auto: a lone pane is not a "tab", so it goes without a strip; two or more
|
||||
// need one to switch between them.
|
||||
// Auto: two or more panes need a strip to switch between them.
|
||||
if (zone.shown.length > 1) {
|
||||
return true
|
||||
}
|
||||
|
||||
// …except a lone MAIN tile once a second main zone exists. Dragging a
|
||||
// session out of the chat strip into its own zone left the workspace alone
|
||||
// in main: the tile kept its tab (stranded), main lost its tab and its "+",
|
||||
// and two chats side by side read as "my tabs disappeared". Tiles are a
|
||||
// tabbed workflow, so every main zone keeps its strip while there is more
|
||||
// than one; a chat that is the whole window is still chromeless.
|
||||
return Boolean(zone.siblingMainZone) && zone.shown.some(pane => pane.placement === 'main')
|
||||
// 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')
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -120,13 +115,10 @@ export function tabStripVisibleForZone(zone: {
|
||||
paneFor: (id: string) => Contribution | undefined
|
||||
/** Panes currently rendered as chips. */
|
||||
shown: readonly string[]
|
||||
/** Another zone in the layout hosts a main tile too (`$mainTileZoneCount`). */
|
||||
siblingMainZone: boolean
|
||||
}): boolean {
|
||||
return resolveTabStripVisible({
|
||||
headerVeto: paneChrome(zone.paneFor(zone.active)).headerVeto,
|
||||
mode: effectiveTabStripMode(zone.mode),
|
||||
siblingMainZone: zone.siblingMainZone,
|
||||
shown: zone.shown.map(id => {
|
||||
const chrome = paneChrome(zone.paneFor(id))
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ import type { DropPosition, GroupNode } from '../model'
|
||||
import {
|
||||
$dropHint,
|
||||
$hiddenTreePanes,
|
||||
$mainTileZoneCount,
|
||||
$narrowViewport,
|
||||
$newSessionTabAction,
|
||||
$panesWithCloser,
|
||||
@@ -264,9 +263,6 @@ export function TreeGroup({
|
||||
|
||||
const hiddenPanes = useStore($hiddenTreePanes)
|
||||
const narrow = useStore($narrowViewport)
|
||||
// A count that moves only when a main zone appears or goes — NOT the tree
|
||||
// itself (see the note above `targetPane` on why zones never subscribe to it).
|
||||
const mainTileZoneCount = useStore($mainTileZoneCount)
|
||||
const workspaceMode = useStore($workspaceMode)
|
||||
const workspaceOwnerKey = useStore($workspaceOwnerKey)
|
||||
const newSessionTabAction = useStore($newSessionTabAction)
|
||||
@@ -381,8 +377,7 @@ export function TreeGroup({
|
||||
isCollapsePane,
|
||||
mode: node.tabStrip,
|
||||
paneFor,
|
||||
shown,
|
||||
siblingMainZone: mainTileZoneCount > (shown.some(id => paneChrome(paneFor(id)).placement === 'main') ? 1 : 0)
|
||||
shown
|
||||
})
|
||||
|
||||
// A group collapses ALONG its parent split's axis. In a row that means the
|
||||
|
||||
@@ -8,7 +8,7 @@ import { atom, computed, type ReadableAtom } from 'nanostores'
|
||||
|
||||
import { SIDEBAR_COLLAPSE_MEDIA_QUERY } from '@/app/layout-constants'
|
||||
import { setPluginEnabled } from '@/contrib/plugins-store'
|
||||
import { $registryVersion, registry } from '@/contrib/registry'
|
||||
import { registry } from '@/contrib/registry'
|
||||
import { translateNow } from '@/i18n'
|
||||
import { LAYOUT_KEYS } from '@/lib/layout-persistence'
|
||||
import { Codecs } from '@/lib/persisted'
|
||||
@@ -847,21 +847,6 @@ export function shownPanesInGroup(group: { panes: readonly string[] }): string[]
|
||||
})
|
||||
}
|
||||
|
||||
/** How many zones currently show a MAIN tile (a chat, a page, a preview). A
|
||||
* count, not a list, so it notifies only when a main zone appears or goes —
|
||||
* every TreeGroup reads it, and a sash drag rewrites the tree once per frame.
|
||||
* Registry-versioned because a freshly adopted session tile is in the tree
|
||||
* before its contribution registers `placement: 'main'`. */
|
||||
export const $mainTileZoneCount = computed(
|
||||
[$layoutTree, $hiddenTreePanes, $registryVersion],
|
||||
(tree: LayoutNode | null) =>
|
||||
tree
|
||||
? groupLeafIds(tree).filter(id =>
|
||||
shownPanesInGroup({ panes: findGroup(tree, id)?.panes ?? [] }).some(isMainStripPane)
|
||||
).length
|
||||
: 0
|
||||
)
|
||||
|
||||
/** Is this zone showing a tab strip right now? The store's adapter over the
|
||||
* shared resolver — TreeGroup answers the same question from its own render
|
||||
* inputs, so the toggle command and the strip on screen cannot disagree about
|
||||
@@ -875,8 +860,7 @@ export function tabStripVisibleForGroup(group: GroupNode): boolean {
|
||||
isCollapsePane,
|
||||
mode: group.tabStrip,
|
||||
paneFor: (id: string) => registered.find(c => c.id === id),
|
||||
shown,
|
||||
siblingMainZone: $mainTileZoneCount.get() > (shown.some(isMainStripPane) ? 1 : 0)
|
||||
shown
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user