Skip to content
Open
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
44 changes: 43 additions & 1 deletion apps/desktop/src/app/chat/sidebar/session-actions-menu.test.tsx
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -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() }))
Expand All @@ -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.',
Expand Down Expand Up @@ -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()

Expand All @@ -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()
})
})
16 changes: 16 additions & 0 deletions apps/desktop/src/app/chat/sidebar/session-actions-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ import {
closeAllTreeTabs,
closeOtherTreeTabs,
closeTreeTabsToRight,
isWorkspaceTabBarHidden,
reloadTreePane,
showWorkspaceTabBar,
treeTabCloseTargets
} from '@/components/pane-shell/tree/store'
import {
Expand Down Expand Up @@ -425,6 +427,20 @@ function useSessionActions({
})}
</>
)}
{isWorkspaceTabBarHidden() && (
<>
<kit.Separator />
{renderActionItem(kit, {
disabled: false,
icon: 'eye',
label: r.showTabBar,
onSelect: () => {
triggerHaptic('selection')
showWorkspaceTabBar()
}
})}
</>
)}
</>
)

Expand Down
23 changes: 23 additions & 0 deletions apps/desktop/src/components/pane-shell/tree/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/i18n/ar.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1645,6 +1645,7 @@ export const ar = defineLocale({
backgroundRunning: 'تعمل في الخلفية',
finishedUnread: 'اكتملت وفيها جديد',
hideTabBar: 'إخفاء شريط التبويبات',
showTabBar: 'إظهار شريط التبويبات',
openInNewTab: 'فتح في تبويب جديد',
openInSplit: 'فتح في تقسيم',
ownedByProfile: profile => `مملوكة للملف الشخصي ${profile}`,
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/i18n/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1637,6 +1637,7 @@ export interface Translations {
archive: string
newWindow: string
hideTabBar: string
showTabBar: string
openInNewTab: string
openInSplit: string
copyIdFailed: string
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2135,6 +2135,7 @@ export const zh: Translations = {
archive: '归档',
newWindow: '新窗口',
hideTabBar: '隐藏标签栏',
showTabBar: '显示标签栏',
openInNewTab: '在新标签页中打开',
openInSplit: '在分屏中打开',
copyIdFailed: '无法复制会话 ID',
Expand Down