feat(ui): let users hide context rail surfaces

A trailing configure button on the rail — outside the sortable list and the
digit shortcuts — opens a dialog that toggles each surface. The choice is
stored as the hidden set so newly added surfaces appear for everyone, and
the rail and the mod+alt+digit switcher share the same visibility filter, so
badges and shortcuts always agree. Hidden surfaces keep their data and stay
reachable from the command palette.
This commit is contained in:
Bohdan Triapitsyn
2026-08-26 16:18:15 +03:00
parent f2ec9b1003
commit 2e49e44205
17 changed files with 195 additions and 2 deletions
@@ -36,6 +36,7 @@ import { cn } from '@/lib/utils';
import { useFeatureFlagsStore } from '@/stores/useFeatureFlagsStore';
import { useGitStatus } from '@/stores/useGitStore';
import { normalizeContextPanelDirectoryKey, useUIStore } from '@/stores/useUIStore';
import { ContextRailSurfacesDialog } from './ContextRailSurfacesDialog';
const RAIL_TOOLTIP_DELAY_MS = 150;
// Hold the surface-switch modifier for this long before revealing the order
@@ -161,6 +162,7 @@ export const ContextPanelRail: React.FC = () => {
const panelState = useUIStore((state) => (directoryKey ? state.contextPanelByDirectory[directoryKey] : undefined));
const workStatusPanelVisible = useUIStore((state) => state.workStatusPanelVisible);
const contextRailOrder = useUIStore((state) => state.contextRailOrder);
const contextRailHiddenSurfaces = useUIStore((state) => state.contextRailHiddenSurfaces);
const setContextRailOrder = useUIStore((state) => state.setContextRailOrder);
const openContextSurface = useUIStore((state) => state.openContextSurface);
const shortcutOverrides = useUIStore((state) => state.shortcutOverrides);
@@ -256,12 +258,15 @@ export const ContextPanelRail: React.FC = () => {
const surfaces = React.useMemo(() => {
return getVisibleContextRailSurfaces({
railOrder: contextRailOrder,
hiddenSurfaces: contextRailHiddenSurfaces,
planModeEnabled,
isVSCode: isVSCodeRuntime(),
screenWidth,
tabs,
});
}, [contextRailOrder, planModeEnabled, screenWidth, tabs]);
}, [contextRailHiddenSurfaces, contextRailOrder, planModeEnabled, screenWidth, tabs]);
const [isSurfacesDialogOpen, setIsSurfacesDialogOpen] = React.useState(false);
const handleDragEnd = React.useCallback((event: DragEndEvent) => {
const { active, over } = event;
@@ -331,6 +336,24 @@ export const ContextPanelRail: React.FC = () => {
})}
</SortableContext>
</DndContext>
{/* Outside the sortable list on purpose: this button takes no digit,
cannot be dragged, and configures the rail rather than living on it. */}
<Tooltip delayDuration={RAIL_TOOLTIP_DELAY_MS}>
<TooltipTrigger asChild>
<button
type="button"
aria-label={t('contextRail.configure.open')}
onClick={() => setIsSurfacesDialogOpen(true)}
className="flex h-9 w-9 items-center justify-center rounded-md text-muted-foreground/70 transition-colors hover:text-foreground"
>
<Icon name="equalizer-2" className="h-[18px] w-[18px]" />
</button>
</TooltipTrigger>
<TooltipContent side="left" sideOffset={8}>
{t('contextRail.configure.open')}
</TooltipContent>
</Tooltip>
<ContextRailSurfacesDialog open={isSurfacesDialogOpen} onOpenChange={setIsSurfacesDialogOpen} />
</nav>
);
};
@@ -0,0 +1,78 @@
import React from 'react';
import { useI18n } from '@/lib/i18n';
import { useUIStore } from '@/stores/useUIStore';
import { SettingsCheckboxRow } from '@/components/sections/shared/SettingsSection';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { sortContextSurfaces } from '@/lib/surfaces/registry';
/**
* Which surfaces the context rail shows. Everything is on by default and the
* choice is stored as the *hidden* set, so a surface added in a later release
* appears for everyone rather than staying invisible to whoever had saved
* settings before it existed. Hidden surfaces also leave the digit shortcuts
* (the rail and the shortcut share one visibility filter).
*/
export const ContextRailSurfacesDialog: React.FC<{
open: boolean;
onOpenChange: (open: boolean) => void;
}> = ({ open, onOpenChange }) => {
const { t } = useI18n();
const contextRailOrder = useUIStore((state) => state.contextRailOrder);
const hidden = useUIStore((state) => state.contextRailHiddenSurfaces);
const setSurfaceVisible = useUIStore((state) => state.setContextRailSurfaceVisible);
const setHiddenSurfaces = useUIStore((state) => state.setContextRailHiddenSurfaces);
// The full registry in the user's rail order — including surfaces a runtime
// filter currently drops, so a choice made on desktop is editable anywhere.
const surfaces = React.useMemo(() => sortContextSurfaces(contextRailOrder), [contextRailOrder]);
const allVisible = hidden.length === 0;
const noneVisible = surfaces.every((surface) => hidden.includes(surface.id));
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>{t('contextRail.configure.dialogTitle')}</DialogTitle>
<DialogDescription>{t('contextRail.configure.dialogDescription')}</DialogDescription>
</DialogHeader>
<div className="flex flex-col">
{surfaces.map((surface) => (
<SettingsCheckboxRow
key={surface.id}
settingsItem={`layout.context-rail.surface.${surface.id}`}
checked={!hidden.includes(surface.id)}
onChange={(checked) => setSurfaceVisible(surface.id, checked)}
label={t(surface.labelKey)}
ariaLabel={t(surface.labelKey)}
/>
))}
</div>
{!allVisible ? (
<div className="flex items-center justify-between border-t pt-3">
{noneVisible ? (
<span className="text-xs text-destructive">{t('contextRail.configure.noneWarning')}</span>
) : <span />}
<Button
variant="link"
size="xs"
onClick={() => setHiddenSurfaces([])}
className="normal-case text-muted-foreground hover:text-foreground"
>
{t('contextRail.configure.showAll')}
</Button>
</div>
) : null}
</DialogContent>
</Dialog>
);
};