feat(ui): add right-click close menu for context panel tabs (#3217)

Resolves openchamber/openchamber#3123.

The right panel's tab strip (browser, files, chat, and other
multi-instance surfaces) now supports a right-click context menu with
Close, Close others, Close to the left, Close to the right, and Close
all. These act on the current surface's tabs and reuse the new bulk
close action, so closing the active surface's last tab still closes the
panel while other surfaces remain.

- Add closeContextPanelTabs(directory, ids) to useUIStore
- Add opt-in tabContextMenu prop to SortableTabsStrip (no impact on other consumers)
- Wire the menu in ContextPanel with full i18n coverage across 11 locales
- Cover the bulk close with store tests
This commit is contained in:
Angel Davila
2026-08-29 00:35:24 +03:00
committed by GitHub
parent cbe908009b
commit a78ecf8a7c
18 changed files with 251 additions and 23 deletions
@@ -3,6 +3,7 @@ import React from 'react';
import { FileTypeIcon } from '@/components/icons/FileTypeIcon';
import { DiffViewIcon } from '@/components/icons/DiffIcon';
import { Button } from '@/components/ui/button';
import { ContextMenuItem, ContextMenuSeparator } from '@/components/ui/context-menu';
import { SortableTabsStrip } from '@/components/ui/sortable-tabs-strip';
import { PullRequestView } from '@/components/views/PullRequestView';
import { TerminalView } from '@/components/views/TerminalView';
@@ -979,6 +980,47 @@ export const ContextPanel: React.FC = () => {
const isFileTabActive = activeTab?.mode === 'file';
const closeContextPanelTabs = useUIStore((state) => state.closeContextPanelTabs);
const renderTabContextMenu = React.useCallback(
(args: { id: string; index: number; allIds: string[]; close: () => void }): React.ReactNode => {
if (!directoryKey) {
return null;
}
const { id, index, allIds, close } = args;
const closeOthers = () => closeContextPanelTabs(directoryKey, allIds.filter((tabId) => tabId !== id));
const closeToLeft = () => closeContextPanelTabs(directoryKey, allIds.slice(0, index));
const closeToRight = () => closeContextPanelTabs(directoryKey, allIds.slice(index + 1));
const closeAll = () => closeContextPanelTabs(directoryKey, allIds);
const hasOthers = allIds.length > 1;
const isFirst = index === 0;
const isLast = index === allIds.length - 1;
return (
<>
<ContextMenuItem onClick={close}>
<Icon name="close" className="mr-2 size-4" />
{t('contextPanel.tab.menu.close')}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={closeOthers} disabled={!hasOthers}>
{t('contextPanel.tab.menu.closeOthers')}
</ContextMenuItem>
<ContextMenuItem onClick={closeToLeft} disabled={isFirst}>
{t('contextPanel.tab.menu.closeToLeft')}
</ContextMenuItem>
<ContextMenuItem onClick={closeToRight} disabled={isLast}>
{t('contextPanel.tab.menu.closeToRight')}
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem onClick={closeAll} disabled={!hasOthers}>
<Icon name="close-circle" className="mr-2 size-4" />
{t('contextPanel.tab.menu.closeAll')}
</ContextMenuItem>
</>
);
},
[closeContextPanelTabs, directoryKey, t],
);
const header = (
<header className="flex h-10 items-stretch border-b border-border">
{isMultiInstanceMode ? (
@@ -1005,6 +1047,7 @@ export const ContextPanel: React.FC = () => {
}}
layoutMode="scrollable"
variant="default"
tabContextMenu={renderTabContextMenu}
/>
) : (
<div className="flex min-w-0 flex-1 items-center gap-1.5 px-3">