feat(desktop): sessions/bots tabs are show/hide chrome — no close gestures, right-click + Cmd-K toggles
Builds on #89551 (@calvinnwq, cherry-picked): his showCloseButton flag hid the hover X; this completes the model so standing chrome can never be closed at all, only shown/hidden (#89546). - hideOnly pane chrome (sessions + Bots): no hover X, no middle/meta click close, no Close verbs in the tab menu, excluded from close-others/right/all sweeps - zone right-click menu gains Show/Hide rows for the strip's chrome tabs (Hide bots / Show sessions, localized in 6 locales) - Cmd-K palette: auto-registered "Toggle <tab> tab" rows for every hideOnly pane, on-screen truth semantics, plugin panes included via registry subscription - hides persist across launches (survive the enforced dock re-adopt); reveal intent and Layout reset clear them - last-visible-tab guard: hiding the zone's last shown tab is refused with a toast, so the strip can never become an empty dead zone
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
||||
removeTreePane,
|
||||
resetLayoutTree,
|
||||
revealTreePane,
|
||||
setStripTabHidden,
|
||||
togglePaneVisible,
|
||||
watchContributedPanes
|
||||
} from '@/components/pane-shell/tree/store'
|
||||
@@ -38,6 +39,7 @@ import { Slot } from '@/contrib/react/slot'
|
||||
import { useContributions } from '@/contrib/react/use-contributions'
|
||||
import { registry } from '@/contrib/registry'
|
||||
import { discoverRuntimePlugins } from '@/contrib/runtime-loader'
|
||||
import { translateNow } from '@/i18n'
|
||||
import { NEW_SESSION_TITLE, sessionTitle as storedSessionTitle } from '@/lib/chat-runtime'
|
||||
import { Download, FileText, LayoutDashboard, PanelBottom, Terminal, Upload, Zap } from '@/lib/icons'
|
||||
import { type KeybindContribution, KEYBINDS_AREA } from '@/lib/keybinds/actions'
|
||||
@@ -155,6 +157,9 @@ registry.registerMany([
|
||||
dock: { pane: 'workspace', pos: 'left' },
|
||||
revealAliases: ['chat-sidebar'],
|
||||
showCloseButton: false,
|
||||
// Standing chrome: no close gestures at all — the tab is shown/hidden
|
||||
// (zone menu Show/Hide rows + the auto-registered ⌘K toggle below).
|
||||
hideOnly: true,
|
||||
width: `${SIDEBAR_DEFAULT_WIDTH}px`,
|
||||
minWidth: `${SIDEBAR_DEFAULT_WIDTH}px`,
|
||||
maxWidth: `${SIDEBAR_MAX_WIDTH}px`
|
||||
@@ -673,6 +678,63 @@ registry.register(
|
||||
})
|
||||
)
|
||||
|
||||
// Hide-only chrome tabs (sessions / Bots) get a ⌘K toggle each — the palette
|
||||
// door onto the same show/hide the zone menu offers. Auto-registered from the
|
||||
// panes area so a plugin's hideOnly pane (Bots registers at plugin load, after
|
||||
// this module runs) gets its row for free; disposers keep it in step when a
|
||||
// plugin unloads. Registry writes during a subscriber callback are safe (the
|
||||
// registry snapshots per-area and re-notifies), and re-registering the same
|
||||
// palette id replaces the row instead of stacking duplicates.
|
||||
{
|
||||
const stripTabToggles = new Map<string, () => void>()
|
||||
|
||||
const syncStripTabToggles = () => {
|
||||
const hideOnlyPanes = registry
|
||||
.getArea('panes')
|
||||
.filter(c => (c.data as { hideOnly?: boolean } | undefined)?.hideOnly)
|
||||
const wanted = new Set(hideOnlyPanes.map(c => c.id))
|
||||
|
||||
for (const [paneId, dispose] of stripTabToggles) {
|
||||
if (!wanted.has(paneId)) {
|
||||
dispose()
|
||||
stripTabToggles.delete(paneId)
|
||||
}
|
||||
}
|
||||
|
||||
for (const pane of hideOnlyPanes) {
|
||||
if (stripTabToggles.has(pane.id)) {
|
||||
continue
|
||||
}
|
||||
|
||||
const title = String(pane.title ?? pane.id)
|
||||
|
||||
stripTabToggles.set(
|
||||
pane.id,
|
||||
registry.register(
|
||||
paletteToggle({
|
||||
id: `strip-tab.${pane.id}`,
|
||||
label: translateNow('zones.toggleStripTab', title),
|
||||
icon: LayoutDashboard,
|
||||
keywords: [title.toLowerCase(), 'tab', 'pane', 'sidebar', 'show', 'hide'],
|
||||
// On-screen truth, same contract as the logs toggle above.
|
||||
get: () => isPaneVisible(pane.id),
|
||||
set: visible => {
|
||||
if (visible) {
|
||||
revealTreePane(pane.id)
|
||||
} else {
|
||||
setStripTabHidden(pane.id, true)
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
syncStripTabToggles()
|
||||
registry.subscribeArea('panes', syncStripTabToggles)
|
||||
}
|
||||
|
||||
// YOLO (dangerous-command approval bypass) is a status-bar zap and a /yolo
|
||||
// command; ⌘K is the third door onto the SAME store function, so a user who
|
||||
// lives in the palette never has to hunt for the pill.
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { registry } from '@/contrib/registry'
|
||||
|
||||
import { allPaneIds, group, split } from './model'
|
||||
import {
|
||||
$hiddenStripTabs,
|
||||
$hiddenTreePanes,
|
||||
$layoutTree,
|
||||
closeAllTreeTabs,
|
||||
hideOnlyZoneTabs,
|
||||
isHideOnlyPane,
|
||||
revealTreePane,
|
||||
setStripTabHidden,
|
||||
treeTabCloseTargets
|
||||
} from './store'
|
||||
|
||||
vi.mock('@/store/notifications', () => ({ notify: vi.fn() }))
|
||||
|
||||
import { notify } from '@/store/notifications'
|
||||
|
||||
const disposers: (() => void)[] = []
|
||||
|
||||
function registerPane(id: string, data: Record<string, unknown>) {
|
||||
disposers.push(registry.register({ area: 'panes', data, id, render: () => null, title: id }))
|
||||
}
|
||||
|
||||
/** The SESSIONS | BOTS shape: both hide-only chrome tabs stacked in one zone. */
|
||||
function sessionsBotsTree() {
|
||||
registerPane('sessions', { placement: 'left', hideOnly: true })
|
||||
registerPane('hermes-bots:pane', { placement: 'left', hideOnly: true })
|
||||
registerPane('workspace', { placement: 'main', uncloseable: true })
|
||||
$layoutTree.set(
|
||||
split('row', [
|
||||
group(['sessions', 'hermes-bots:pane'], { active: 'sessions', id: 'g-side' }),
|
||||
group(['workspace'], { active: 'workspace', id: 'g-main' })
|
||||
])
|
||||
)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear()
|
||||
$hiddenStripTabs.set(new Set())
|
||||
$hiddenTreePanes.set(new Set())
|
||||
vi.mocked(notify).mockReset()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
disposers.splice(0).forEach(dispose => dispose())
|
||||
})
|
||||
|
||||
describe('hide-only strip tabs', () => {
|
||||
it('hides and shows a chrome tab, keeping the pane in the tree', () => {
|
||||
sessionsBotsTree()
|
||||
|
||||
expect(setStripTabHidden('hermes-bots:pane', true)).toBe(true)
|
||||
expect($hiddenTreePanes.get()).toContain('hermes-bots:pane')
|
||||
expect($hiddenStripTabs.get()).toContain('hermes-bots:pane')
|
||||
// Hidden, not dismissed: the pane stays in the layout tree.
|
||||
expect(allPaneIds($layoutTree.get()!)).toContain('hermes-bots:pane')
|
||||
|
||||
expect(setStripTabHidden('hermes-bots:pane', false)).toBe(true)
|
||||
expect($hiddenTreePanes.get()).not.toContain('hermes-bots:pane')
|
||||
expect($hiddenStripTabs.get()).not.toContain('hermes-bots:pane')
|
||||
})
|
||||
|
||||
it('refuses to hide the zone last visible tab', () => {
|
||||
sessionsBotsTree()
|
||||
setStripTabHidden('hermes-bots:pane', true)
|
||||
|
||||
// Sessions is now the only visible tab in the zone — the hide is refused
|
||||
// and the user is told, so the zone can never become an empty dead strip.
|
||||
expect(setStripTabHidden('sessions', true)).toBe(false)
|
||||
expect($hiddenTreePanes.get()).not.toContain('sessions')
|
||||
expect(vi.mocked(notify)).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('persists hides and clears them on reveal', () => {
|
||||
sessionsBotsTree()
|
||||
setStripTabHidden('hermes-bots:pane', true)
|
||||
|
||||
const persisted = JSON.parse(window.localStorage.getItem('hermes.desktop.hiddenStripTabs.v1') ?? '[]')
|
||||
|
||||
expect(persisted).toContain('hermes-bots:pane')
|
||||
|
||||
// Reveal intent (⌘K toggle on, a programmatic reveal) beats the hide —
|
||||
// including the persisted record, so the tab can't pop back hidden on the
|
||||
// next launch while visibly on screen now.
|
||||
revealTreePane('hermes-bots:pane')
|
||||
expect($hiddenTreePanes.get()).not.toContain('hermes-bots:pane')
|
||||
expect(window.localStorage.getItem('hermes.desktop.hiddenStripTabs.v1')).toBeNull()
|
||||
})
|
||||
|
||||
it('lists the zone hide-only tabs with live hidden state for the menu', () => {
|
||||
sessionsBotsTree()
|
||||
setStripTabHidden('hermes-bots:pane', true)
|
||||
|
||||
expect(hideOnlyZoneTabs('g-side')).toEqual([
|
||||
{ hidden: false, id: 'sessions', title: 'sessions' },
|
||||
{ hidden: true, id: 'hermes-bots:pane', title: 'hermes-bots:pane' }
|
||||
])
|
||||
expect(hideOnlyZoneTabs('g-main')).toEqual([])
|
||||
})
|
||||
|
||||
it('excludes hide-only tabs from every close verb', () => {
|
||||
registerPane('sessions', { placement: 'left', hideOnly: true })
|
||||
registerPane('hermes-bots:pane', { placement: 'left', hideOnly: true })
|
||||
registerPane('session-tile:x', { placement: 'main' })
|
||||
$layoutTree.set(
|
||||
group(['sessions', 'hermes-bots:pane', 'session-tile:x'], { active: 'sessions', id: 'g-mixed' })
|
||||
)
|
||||
|
||||
expect(isHideOnlyPane('sessions')).toBe(true)
|
||||
// Close-others measured from the tile must not sweep standing chrome.
|
||||
expect(treeTabCloseTargets('session-tile:x')).toEqual({ all: 1, others: 0, right: 0 })
|
||||
// Close-all leaves both chrome tabs in the tree.
|
||||
closeAllTreeTabs('sessions')
|
||||
expect(allPaneIds($layoutTree.get()!)).toContain('sessions')
|
||||
expect(allPaneIds($layoutTree.get()!)).toContain('hermes-bots:pane')
|
||||
expect(allPaneIds($layoutTree.get()!)).not.toContain('session-tile:x')
|
||||
})
|
||||
})
|
||||
@@ -67,6 +67,11 @@ interface PaneChrome extends PaneSizing {
|
||||
uncloseable?: boolean
|
||||
/** Hide the hover ✕ while retaining explicit close behavior for this pane. */
|
||||
showCloseButton?: boolean
|
||||
/** Standing chrome tab (sessions / Bots) whose tab shows NO ✕ and no Close
|
||||
* verbs — it is shown/hidden instead (the zone menu's Show/Hide rows and a
|
||||
* ⌘K toggle, via `setStripTabHidden`). Close was too destructive for these:
|
||||
* an accidental ✕ removed Bot Mode until the next launch. */
|
||||
hideOnly?: boolean
|
||||
/** Wrap this pane's TAB (e.g. in a domain context menu — a session tile's
|
||||
* pin/branch/rename/archive/delete). The wrapper must render `tab` as its
|
||||
* interactive child; the zone's own strip menu still owns non-tab space. */
|
||||
|
||||
@@ -49,6 +49,7 @@ import {
|
||||
closeTabPane,
|
||||
closeTreeTabsToRight,
|
||||
collapseTreePane,
|
||||
hideOnlyZoneTabs,
|
||||
isCollapsePane,
|
||||
isMainStripPane,
|
||||
isSessionStripPane,
|
||||
@@ -56,6 +57,7 @@ import {
|
||||
reloadTreePane,
|
||||
restoreTreePane,
|
||||
SESSION_TILE_DRAG,
|
||||
setStripTabHidden,
|
||||
setTreeGroupHeaderHidden,
|
||||
setTreeGroupMinimized,
|
||||
treeTabCloseTargets
|
||||
@@ -129,6 +131,30 @@ function ZoneMenu({
|
||||
onCloseOthers: () => closeOtherTreeTabs(targetId),
|
||||
onCloseToRight: () => closeTreeTabsToRight(targetId)
|
||||
})}
|
||||
{(() => {
|
||||
// Show/hide rows for the zone's hide-only chrome tabs (sessions /
|
||||
// Bots) — their Close replacement. Resolved when the menu OPENS,
|
||||
// same no-subscription contract as the close-verb counts above.
|
||||
const hideOnly = hideOnlyZoneTabs(nodeId)
|
||||
|
||||
if (hideOnly.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<kit.Separator />
|
||||
{hideOnly.map(tab =>
|
||||
renderActionItem(kit, {
|
||||
icon: tab.hidden ? 'eye' : 'eye-closed',
|
||||
key: `strip-tab-${tab.id}`,
|
||||
label: tab.hidden ? t.zones.showStripTab(tab.title) : t.zones.hideStripTab(tab.title),
|
||||
onSelect: () => setStripTabHidden(tab.id, !tab.hidden)
|
||||
})
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
<kit.Separator />
|
||||
{renderActionItem(kit, {
|
||||
icon: headerHidden ? 'eye' : 'eye-closed',
|
||||
@@ -287,11 +313,13 @@ export function TreeGroup({
|
||||
const targetPane = () => menuPane ?? activeId
|
||||
|
||||
// Close targets the right-clicked chip (falling back to the active pane);
|
||||
// only panes that declare `uncloseable` (the main workspace) are exempt.
|
||||
// panes that declare `uncloseable` (the main workspace) or `hideOnly`
|
||||
// (sessions / Bots — show/hide replaces Close) are exempt.
|
||||
const closable = () => {
|
||||
const paneId = targetPane()
|
||||
const chrome = paneChrome(paneFor(paneId))
|
||||
|
||||
return paneChrome(paneFor(paneId)).uncloseable ? undefined : paneId
|
||||
return chrome.uncloseable || chrome.hideOnly ? undefined : paneId
|
||||
}
|
||||
|
||||
// The zone hosting the uncloseable workspace never minimizes — collapsing
|
||||
@@ -304,8 +332,12 @@ export function TreeGroup({
|
||||
|
||||
// A pane whose store owns Close keeps the gesture even when the pane itself
|
||||
// is uncloseable — the workspace tab empties to a fresh draft rather than
|
||||
// leaving the tree.
|
||||
const closeableTab = (paneId: string) => !paneChrome(paneFor(paneId)).uncloseable || panesWithCloser.has(paneId)
|
||||
// leaving the tree. Hide-only chrome (sessions / Bots) opts out of every
|
||||
// close gesture: its tabs are shown/hidden (zone menu, ⌘K), never closed —
|
||||
// an accidental ✕ on standing chrome removed Bot Mode until the next launch.
|
||||
const closeableTab = (paneId: string) =>
|
||||
!paneChrome(paneFor(paneId)).hideOnly &&
|
||||
(!paneChrome(paneFor(paneId)).uncloseable || panesWithCloser.has(paneId))
|
||||
|
||||
// A pane's own live label when it has one, else its registered string.
|
||||
const tabLabel = (paneId: string) => paneChrome(paneFor(paneId)).tabTitle?.() ?? paneFor(paneId)?.title ?? paneId
|
||||
|
||||
@@ -248,6 +248,72 @@ function recalledEdgeWeights(paneId: string): [number, number] | undefined {
|
||||
return validShare(share) ? [1 - share, share] : undefined
|
||||
}
|
||||
|
||||
// HIDE-ONLY STRIP TABS (`hideOnly` chrome: sessions / Bots) — standing chrome
|
||||
// whose tab must never grow a ✕. Show/hide replaces Close for them: the zone
|
||||
// menu's Show/Hide rows and the auto-registered ⌘K toggles both land here.
|
||||
// Persisted separately from `$hiddenTreePanes` (whose persistence each side
|
||||
// binding owns) so a hidden Bots tab stays hidden across launches even though
|
||||
// dock enforcement re-adopts the pane into the sessions zone every boot.
|
||||
const HIDDEN_STRIP_TAB_KEY = 'hermes.desktop.hiddenStripTabs.v1'
|
||||
|
||||
export const $hiddenStripTabs = atom<ReadonlySet<string>>(new Set(readJson<string[]>(HIDDEN_STRIP_TAB_KEY) ?? []))
|
||||
|
||||
function saveHiddenStripTabs(next: ReadonlySet<string>) {
|
||||
$hiddenStripTabs.set(next)
|
||||
writeJson(HIDDEN_STRIP_TAB_KEY, next.size === 0 ? null : [...next])
|
||||
}
|
||||
|
||||
export function isStripTabHidden(paneId: string): boolean {
|
||||
return $hiddenStripTabs.get().has(paneId)
|
||||
}
|
||||
|
||||
/** Would hiding `paneId` leave its zone with no visible tab? Hiding the last
|
||||
* one strands an empty zone (or collapses the whole sidebar with no strip
|
||||
* left to right-click), so the setter refuses and says why. */
|
||||
function isLastShownInGroup(paneId: string): boolean {
|
||||
const tree = $layoutTree.get()
|
||||
const group = tree ? findGroupOfPane(tree, paneId) : null
|
||||
|
||||
if (!group) {
|
||||
return false
|
||||
}
|
||||
|
||||
const hidden = $hiddenTreePanes.get()
|
||||
|
||||
return !group.panes.some(id => id !== paneId && !hidden.has(id))
|
||||
}
|
||||
|
||||
/** Show/hide a hide-only chrome tab (the Close replacement for `hideOnly`
|
||||
* panes). Returns false when the hide was refused — the zone must keep at
|
||||
* least one visible tab, so the LAST shown tab can't be hidden. */
|
||||
export function setStripTabHidden(paneId: string, hidden: boolean): boolean {
|
||||
if (hidden && isLastShownInGroup(paneId)) {
|
||||
notify({
|
||||
kind: 'info',
|
||||
title: translateNow('zones.lastTabKeptTitle'),
|
||||
message: translateNow('zones.lastTabKeptBody')
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
const next = toggledSet($hiddenStripTabs.get(), paneId, hidden)
|
||||
|
||||
if (next) {
|
||||
saveHiddenStripTabs(next)
|
||||
}
|
||||
|
||||
setTreePaneHidden(paneId, hidden)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// Boot hydration: re-apply persisted hides through the same chrome-hidden set
|
||||
// the strips render from ($hiddenTreePanes starts empty every launch).
|
||||
for (const paneId of $hiddenStripTabs.get()) {
|
||||
setTreePaneHidden(paneId, true)
|
||||
}
|
||||
|
||||
const paneClosers: Record<string, () => void> = {}
|
||||
const paneOpeners: Record<string, () => void> = {}
|
||||
|
||||
@@ -409,6 +475,12 @@ const isUncloseablePane = (paneId: string): boolean =>
|
||||
(registry.getArea('panes').find(c => c.id === paneId)?.data as { uncloseable?: boolean } | undefined)?.uncloseable
|
||||
)
|
||||
|
||||
/** Hide-only chrome tabs (sessions / Bots): excluded from every close verb —
|
||||
* Close-others / Close-all sweeping the sessions strip must not take standing
|
||||
* chrome with it. They hide through `setStripTabHidden` instead. */
|
||||
export const isHideOnlyPane = (paneId: string): boolean =>
|
||||
Boolean((registry.getArea('panes').find(c => c.id === paneId)?.data as { hideOnly?: boolean } | undefined)?.hideOnly)
|
||||
|
||||
/** A pane that belongs to a CHAT tab strip — the workspace or a session tile.
|
||||
* Chat surfaces only: this gates where a session may DOCK (drops, ⌘T's "+"),
|
||||
* not which zones the generic tab verbs serve — that's `isMainStripPane`. */
|
||||
@@ -506,8 +578,8 @@ function closeableTreeSiblings(paneId: string): { others: string[]; right: strin
|
||||
const idx = panes.indexOf(paneId)
|
||||
|
||||
return {
|
||||
others: panes.filter(id => id !== paneId && !isUncloseablePane(id)),
|
||||
right: panes.filter((id, i) => i > idx && !isUncloseablePane(id))
|
||||
others: panes.filter(id => id !== paneId && !isUncloseablePane(id) && !isHideOnlyPane(id)),
|
||||
right: panes.filter((id, i) => i > idx && !isUncloseablePane(id) && !isHideOnlyPane(id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,7 +587,11 @@ function closeableTreeSiblings(paneId: string): { others: string[]; right: strin
|
||||
export function treeTabCloseTargets(paneId: string): { all: number; others: number; right: number } {
|
||||
const { others, right } = closeableTreeSiblings(paneId)
|
||||
|
||||
return { all: others.length + (isUncloseablePane(paneId) ? 0 : 1), others: others.length, right: right.length }
|
||||
return {
|
||||
all: others.length + (isUncloseablePane(paneId) || isHideOnlyPane(paneId) ? 0 : 1),
|
||||
others: others.length,
|
||||
right: right.length
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -557,7 +633,32 @@ export function closeAllTreeTabs(paneId: string): void {
|
||||
const tree = $layoutTree.get()
|
||||
const panes = (tree ? findGroupOfPane(tree, paneId) : null)?.panes ?? []
|
||||
|
||||
panes.filter(id => !isUncloseablePane(id)).forEach(closeTabPane)
|
||||
panes.filter(id => !isUncloseablePane(id) && !isHideOnlyPane(id)).forEach(closeTabPane)
|
||||
}
|
||||
|
||||
/** Hide-only chrome tabs in `groupId` (sessions / Bots), with live hidden
|
||||
* state — the zone menu's Show/Hide rows. Resolved when the menu OPENS (same
|
||||
* contract as the close-verb counts), never subscribed from a zone render. */
|
||||
export function hideOnlyZoneTabs(groupId: string): { hidden: boolean; id: string; title: string }[] {
|
||||
const tree = $layoutTree.get()
|
||||
const group = tree ? findGroup(tree, groupId) : null
|
||||
|
||||
if (!group) {
|
||||
return []
|
||||
}
|
||||
|
||||
const panes = registry.getArea('panes')
|
||||
const hidden = $hiddenTreePanes.get()
|
||||
|
||||
return group.panes.flatMap(id => {
|
||||
const pane = panes.find(p => p.id === id)
|
||||
|
||||
if (!(pane?.data as { hideOnly?: boolean } | undefined)?.hideOnly) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [{ hidden: hidden.has(id), id, title: String(pane?.title ?? id) }]
|
||||
})
|
||||
}
|
||||
|
||||
/** Pane ids in the tree under a `${prefix}:` namespace — lets a mirror prune
|
||||
@@ -923,6 +1024,12 @@ export function revealTreePane(paneId: string) {
|
||||
adoptContributedPanes()
|
||||
}
|
||||
|
||||
// Reveal beats a hide too: clear the persisted hide-only record, or the
|
||||
// pane pops back hidden on the next launch even though it's on screen now.
|
||||
if ($hiddenStripTabs.get().has(paneId)) {
|
||||
saveHiddenStripTabs(toggledSet($hiddenStripTabs.get(), paneId, false) ?? $hiddenStripTabs.get())
|
||||
}
|
||||
|
||||
const side = treeSideOfPane(paneId)
|
||||
|
||||
if (side && $collapsedTreeSides.get().has(side)) {
|
||||
@@ -1796,6 +1903,11 @@ export function resetLayoutTree() {
|
||||
// placement back to the app (user-placed pins cleared).
|
||||
saveDismissed(new Set())
|
||||
saveUserPlaced(new Set())
|
||||
// Hide-only chrome tabs (sessions / Bots) come back too — clear their
|
||||
// persisted hides through the setter so $hiddenTreePanes agrees.
|
||||
for (const paneId of [...$hiddenStripTabs.get()]) {
|
||||
setStripTabHidden(paneId, false)
|
||||
}
|
||||
$layoutTree.set(defaultTree)
|
||||
markActivePreset('default')
|
||||
// Owners PRE-PLACE their panes into the fresh default (session tiles stack
|
||||
|
||||
@@ -2327,6 +2327,11 @@ export const ar = defineLocale({
|
||||
zones: {
|
||||
showHeader: 'إظهار الرأس',
|
||||
hideHeader: 'إخفاء الرأس',
|
||||
showStripTab: title => `إظهار ${title}`,
|
||||
hideStripTab: title => `إخفاء ${title}`,
|
||||
lastTabKeptTitle: 'يبقى آخر تبويب',
|
||||
lastTabKeptBody: 'تحتاج هذه المنطقة إلى تبويب مرئي واحد على الأقل. أظهر تبويبا آخر أولا، أو اطو الشريط الجانبي بأكمله.',
|
||||
toggleStripTab: title => `تبديل تبويب ${title}`,
|
||||
minimize: 'تصغير',
|
||||
restore: 'استعادة',
|
||||
closeRunningTitle: 'إغلاق تبويب يعمل؟',
|
||||
|
||||
@@ -2958,6 +2958,11 @@ export const en: Translations = {
|
||||
zones: {
|
||||
showHeader: 'Show header',
|
||||
hideHeader: 'Hide header',
|
||||
showStripTab: title => `Show ${title}`,
|
||||
hideStripTab: title => `Hide ${title}`,
|
||||
lastTabKeptTitle: 'Last tab stays',
|
||||
lastTabKeptBody: 'This zone needs at least one visible tab. Show another tab first, or collapse the whole sidebar.',
|
||||
toggleStripTab: title => `Toggle ${title} tab`,
|
||||
minimize: 'Minimize',
|
||||
restore: 'Restore',
|
||||
closeRunningTitle: 'Close running tab?',
|
||||
|
||||
@@ -2618,6 +2618,11 @@ export const ja = defineLocale({
|
||||
zones: {
|
||||
showHeader: 'ヘッダーを表示',
|
||||
hideHeader: 'ヘッダーを隠す',
|
||||
showStripTab: title => `${title} を表示`,
|
||||
hideStripTab: title => `${title} を隠す`,
|
||||
lastTabKeptTitle: '最後のタブは残ります',
|
||||
lastTabKeptBody: 'このゾーンには少なくとも 1 つの表示タブが必要です。先に別のタブを表示するか、サイドバー全体を折りたたんでください。',
|
||||
toggleStripTab: title => `${title} タブを切り替え`,
|
||||
minimize: '最小化',
|
||||
restore: '復元',
|
||||
reload: '再読み込み',
|
||||
|
||||
@@ -2530,6 +2530,11 @@ export interface Translations {
|
||||
zones: {
|
||||
showHeader: string
|
||||
hideHeader: string
|
||||
showStripTab: (title: string) => string
|
||||
hideStripTab: (title: string) => string
|
||||
lastTabKeptTitle: string
|
||||
lastTabKeptBody: string
|
||||
toggleStripTab: (title: string) => string
|
||||
minimize: string
|
||||
restore: string
|
||||
closeRunningTitle: string
|
||||
|
||||
@@ -2532,6 +2532,11 @@ export const zhHant = defineLocale({
|
||||
zones: {
|
||||
showHeader: '顯示標題列',
|
||||
hideHeader: '隱藏標題列',
|
||||
showStripTab: title => `顯示 ${title}`,
|
||||
hideStripTab: title => `隱藏 ${title}`,
|
||||
lastTabKeptTitle: '保留最後一個分頁',
|
||||
lastTabKeptBody: '此區域至少需要一個可見分頁。請先顯示另一個分頁,或收合整個側邊欄。',
|
||||
toggleStripTab: title => `切換 ${title} 分頁`,
|
||||
minimize: '最小化',
|
||||
restore: '還原',
|
||||
reload: '重新載入',
|
||||
|
||||
@@ -3124,6 +3124,11 @@ export const zh: Translations = {
|
||||
zones: {
|
||||
showHeader: '显示标题栏',
|
||||
hideHeader: '隐藏标题栏',
|
||||
showStripTab: title => `显示 ${title}`,
|
||||
hideStripTab: title => `隐藏 ${title}`,
|
||||
lastTabKeptTitle: '保留最后一个标签',
|
||||
lastTabKeptBody: '该区域至少需要一个可见标签。请先显示另一个标签,或折叠整个侧边栏。',
|
||||
toggleStripTab: title => `切换 ${title} 标签`,
|
||||
minimize: '最小化',
|
||||
restore: '还原',
|
||||
closeRunningTitle: '关闭正在运行的标签?',
|
||||
|
||||
@@ -9887,7 +9887,7 @@ export default {
|
||||
// sessions pane collapses alone without this flag. The zone then keeps
|
||||
// a stranded BOTS tab on screen. The narrow edge overlay mirrors the
|
||||
// zone's tab strip, so the pane stays reachable while collapsed.
|
||||
data: { placement: 'left', width: '260px', collapsible: true, showCloseButton: false, dock: { pane: 'sessions', pos: 'center', enforce: true } },
|
||||
data: { placement: 'left', width: '260px', collapsible: true, showCloseButton: false, hideOnly: true, dock: { pane: 'sessions', pos: 'center', enforce: true } },
|
||||
render: () => jsx(BotsPane, {})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user