From becbed3e00529f2965506f82e54dbeadfb7a7ce9 Mon Sep 17 00:00:00 2001 From: 12312ewqdq <168364894+12312ewqdq@users.noreply.github.com> Date: Sat, 8 Aug 2026 16:08:20 +0800 Subject: [PATCH] fix(desktop): restore hidden tab bar from sidebar row menu --- .../sidebar/session-actions-menu.test.tsx | 44 ++++++++++++++++++- .../app/chat/sidebar/session-actions-menu.tsx | 16 +++++++ .../src/components/pane-shell/tree/store.ts | 23 ++++++++++ apps/desktop/src/i18n/ar.ts | 1 + apps/desktop/src/i18n/en.ts | 1 + apps/desktop/src/i18n/types.ts | 1 + apps/desktop/src/i18n/zh.ts | 1 + 7 files changed, 86 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/app/chat/sidebar/session-actions-menu.test.tsx b/apps/desktop/src/app/chat/sidebar/session-actions-menu.test.tsx index 559d648407bc..9908e0fa1bf4 100644 --- a/apps/desktop/src/app/chat/sidebar/session-actions-menu.test.tsx +++ b/apps/desktop/src/app/chat/sidebar/session-actions-menu.test.tsx @@ -1,6 +1,8 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { atom } from 'nanostores' -import { afterEach, describe, expect, it, vi } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { isWorkspaceTabBarHidden, showWorkspaceTabBar } from '@/components/pane-shell/tree/store' import { SessionActionsMenu } from './session-actions-menu' @@ -14,6 +16,8 @@ vi.mock('@/components/pane-shell/tree/store', () => ({ closeAllTreeTabs: vi.fn(), closeOtherTreeTabs: vi.fn(), closeTreeTabsToRight: vi.fn(), + isWorkspaceTabBarHidden: vi.fn(() => false), + showWorkspaceTabBar: vi.fn(), treeTabCloseTargets: vi.fn(() => null) })) vi.mock('@/hermes', () => ({ renameSession: vi.fn() })) @@ -37,6 +41,7 @@ vi.mock('@/i18n', () => ({ copyIdFailed: 'Failed to copy ID', export: 'Export', hideTabBar: 'Hide tab bar', + showTabBar: 'Show tab bar', pin: 'Pin', rename: 'Rename', renameDesc: 'Leave empty to clear.', @@ -95,6 +100,24 @@ function renderMenu() { } describe('SessionActionsMenu', () => { + beforeEach(() => { + vi.mocked(isWorkspaceTabBarHidden).mockReset().mockReturnValue(false) + vi.mocked(showWorkspaceTabBar).mockReset() + }) + + async function openMenu() { + const trigger = screen.getByRole('button', { name: 'Session actions' }) + + // Radix's dropdown trigger opens on pointerdown (not on the synthetic + // 'click' fireEvent alone would dispatch), so fire the full mouse + // sequence a real click produces. + fireEvent.pointerDown(trigger, { button: 0, pointerType: 'mouse' }) + fireEvent.pointerUp(trigger, { button: 0, pointerType: 'mouse' }) + fireEvent.click(trigger) + + await screen.findByRole('menu') + } + it('opens the dropdown on click without a tooltip on the kebab', async () => { renderMenu() @@ -113,4 +136,23 @@ describe('SessionActionsMenu', () => { expect(screen.getByRole('menuitem', { name: /rename/i })).toBeTruthy() expect(screen.getByRole('menuitem', { name: /archive/i })).toBeTruthy() }) + + it('offers "Show tab bar" only while the workspace tab bar is hidden, and restores it', async () => { + vi.mocked(isWorkspaceTabBarHidden).mockReturnValue(true) + renderMenu() + await openMenu() + + const item = screen.getByRole('menuitem', { name: /show tab bar/i }) + + expect(item).toBeTruthy() + fireEvent.click(item) + expect(vi.mocked(showWorkspaceTabBar)).toHaveBeenCalledTimes(1) + }) + + it('hides "Show tab bar" while the workspace tab bar is visible', async () => { + renderMenu() + await openMenu() + + expect(screen.queryByRole('menuitem', { name: /show tab bar/i })).toBeNull() + }) }) diff --git a/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx b/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx index 0a32e1881f85..610da03d1ca2 100644 --- a/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx +++ b/apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx @@ -7,7 +7,9 @@ import { closeAllTreeTabs, closeOtherTreeTabs, closeTreeTabsToRight, + isWorkspaceTabBarHidden, reloadTreePane, + showWorkspaceTabBar, treeTabCloseTargets } from '@/components/pane-shell/tree/store' import { @@ -425,6 +427,20 @@ function useSessionActions({ })} )} + {isWorkspaceTabBarHidden() && ( + <> + + {renderActionItem(kit, { + disabled: false, + icon: 'eye', + label: r.showTabBar, + onSelect: () => { + triggerHaptic('selection') + showWorkspaceTabBar() + } + })} + + )} ) diff --git a/apps/desktop/src/components/pane-shell/tree/store.ts b/apps/desktop/src/components/pane-shell/tree/store.ts index daf7d77fe233..190a2ae2bb9a 100644 --- a/apps/desktop/src/components/pane-shell/tree/store.ts +++ b/apps/desktop/src/components/pane-shell/tree/store.ts @@ -1578,6 +1578,29 @@ export function collapseTreePane(paneId: string) { } } +/** Read-only: is the workspace zone's header (the main tab bar) explicitly + * hidden? Read at menu-open time via `.get()` — never a render-time + * subscription (layout-tree reads must stay render-free, see tree-group.tsx). + * Only an EXPLICIT hide (headerHidden: true) counts; a lone pane's auto-hide + * default is not a user state to restore. */ +export function isWorkspaceTabBarHidden(): boolean { + const tree = $layoutTree.get() + + return tree ? findGroupOfPane(tree, 'workspace')?.headerHidden === true : false +} + +/** Flip the workspace zone's sticky tab bar back on. The off switch lives on + * the main tab's context menu — unreachable once the bar is hidden — so this + * is the symmetrical restore for the always-reachable sidebar row menu. */ +export function showWorkspaceTabBar(): void { + const tree = $layoutTree.get() + const group = tree ? findGroupOfPane(tree, 'workspace') : null + + if (group) { + setTreeGroupHeaderHidden(group.id, false) + } +} + /** Hide/show a zone's header entirely (double-click gesture). */ export function setTreeGroupHeaderHidden(groupId: string, headerHidden: boolean) { const tree = $layoutTree.get() diff --git a/apps/desktop/src/i18n/ar.ts b/apps/desktop/src/i18n/ar.ts index 65ba2f448ddd..3fb06f98efcf 100644 --- a/apps/desktop/src/i18n/ar.ts +++ b/apps/desktop/src/i18n/ar.ts @@ -1645,6 +1645,7 @@ export const ar = defineLocale({ backgroundRunning: 'تعمل في الخلفية', finishedUnread: 'اكتملت وفيها جديد', hideTabBar: 'إخفاء شريط التبويبات', + showTabBar: 'إظهار شريط التبويبات', openInNewTab: 'فتح في تبويب جديد', openInSplit: 'فتح في تقسيم', ownedByProfile: profile => `مملوكة للملف الشخصي ${profile}`, diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index ee3eeed7df75..d9f30797d52b 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -1943,6 +1943,7 @@ export const en: Translations = { archive: 'Archive', newWindow: 'New window', hideTabBar: 'Hide tab bar', + showTabBar: 'Show tab bar', openInNewTab: 'Open in new tab', openInSplit: 'Open in split', copyIdFailed: 'Could not copy session ID', diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts index d29f3d38e017..492d0e76579f 100644 --- a/apps/desktop/src/i18n/types.ts +++ b/apps/desktop/src/i18n/types.ts @@ -1637,6 +1637,7 @@ export interface Translations { archive: string newWindow: string hideTabBar: string + showTabBar: string openInNewTab: string openInSplit: string copyIdFailed: string diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index bb063e5752d3..f22cea4df803 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -2135,6 +2135,7 @@ export const zh: Translations = { archive: '归档', newWindow: '新窗口', hideTabBar: '隐藏标签栏', + showTabBar: '显示标签栏', openInNewTab: '在新标签页中打开', openInSplit: '在分屏中打开', copyIdFailed: '无法复制会话 ID',