diff --git a/packages/ui/src/components/layout/ContextPanelRail.tsx b/packages/ui/src/components/layout/ContextPanelRail.tsx index 6b3c147a..23308012 100644 --- a/packages/ui/src/components/layout/ContextPanelRail.tsx +++ b/packages/ui/src/components/layout/ContextPanelRail.tsx @@ -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 = () => { })} + {/* Outside the sortable list on purpose: this button takes no digit, + cannot be dragged, and configures the rail rather than living on it. */} + + + + + + {t('contextRail.configure.open')} + + + ); }; diff --git a/packages/ui/src/components/layout/ContextRailSurfacesDialog.tsx b/packages/ui/src/components/layout/ContextRailSurfacesDialog.tsx new file mode 100644 index 00000000..211b465a --- /dev/null +++ b/packages/ui/src/components/layout/ContextRailSurfacesDialog.tsx @@ -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 ( + + + + {t('contextRail.configure.dialogTitle')} + {t('contextRail.configure.dialogDescription')} + + +
+ {surfaces.map((surface) => ( + setSurfaceVisible(surface.id, checked)} + label={t(surface.labelKey)} + ariaLabel={t(surface.labelKey)} + /> + ))} +
+ + {!allVisible ? ( +
+ {noneVisible ? ( + {t('contextRail.configure.noneWarning')} + ) : } + +
+ ) : null} +
+
+ ); +}; diff --git a/packages/ui/src/hooks/useKeyboardShortcuts.ts b/packages/ui/src/hooks/useKeyboardShortcuts.ts index d04a1a9e..0c99f3b0 100644 --- a/packages/ui/src/hooks/useKeyboardShortcuts.ts +++ b/packages/ui/src/hooks/useKeyboardShortcuts.ts @@ -499,6 +499,7 @@ export const useKeyboardShortcuts = () => { const panel = state.contextPanelByDirectory[directory]; const visibleSurfaces = getVisibleContextRailSurfaces({ railOrder: state.contextRailOrder, + hiddenSurfaces: state.contextRailHiddenSurfaces, planModeEnabled: useFeatureFlagsStore.getState().planModeEnabled, isVSCode: isVSCodeRuntime(), screenWidth: window.innerWidth, diff --git a/packages/ui/src/lib/i18n/messages/de.ts b/packages/ui/src/lib/i18n/messages/de.ts index 815186a0..64382772 100644 --- a/packages/ui/src/lib/i18n/messages/de.ts +++ b/packages/ui/src/lib/i18n/messages/de.ts @@ -2995,6 +2995,11 @@ export const dict = { 'gitView.pr.segment.comments': 'Kommentare', 'gitView.pr.comments.addAll': 'Alle hinzufügen', 'contextPanel.mode.pr': 'PR', + 'contextRail.configure.open': 'Panels konfigurieren', + 'contextRail.configure.dialogTitle': 'Leisten-Panels', + 'contextRail.configure.dialogDescription': 'Wähle, welche Panels die Leiste zeigt. Ausgeblendete Panels behalten ihre Daten und bleiben über die Befehlspalette erreichbar.', + 'contextRail.configure.showAll': 'Alle anzeigen', + 'contextRail.configure.noneWarning': 'Alle Panels sind ausgeblendet.', 'contextRail.aria.rail': 'Kontextleiste', 'contextPanel.editorEmpty.title': 'Kein Kontext ausgewählt', 'contextPanel.editorEmpty.description': 'Wählen Sie etwas aus der Seitenleiste aus, um Kontext anzuzeigen.', diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index 62ab1973..169f4c5f 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -1139,6 +1139,11 @@ export const dict = { 'contextPanel.mode.context': 'Context', 'contextPanel.mode.preview': 'Preview', 'contextPanel.mode.browser': 'Browser', + 'contextRail.configure.open': 'Configure panels', + 'contextRail.configure.dialogTitle': 'Rail panels', + 'contextRail.configure.dialogDescription': 'Choose which panels the rail shows. Hidden panels keep their data and stay reachable from the command palette.', + 'contextRail.configure.showAll': 'Show all', + 'contextRail.configure.noneWarning': 'All panels are hidden.', 'contextRail.aria.rail': 'Panel surfaces', 'contextPanel.editorEmpty.title': 'No file open', 'contextPanel.editorEmpty.description': 'Pick a file from the tree to start editing.', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index 4760f8a1..04bbeaa6 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -1140,6 +1140,11 @@ export const dict: Record = { "contextPanel.mode.context": "Contexto", "contextPanel.mode.preview": "Vista previa", "contextPanel.mode.browser": "Navegador", + "contextRail.configure.open": "Configurar paneles", + "contextRail.configure.dialogTitle": "Paneles de la barra", + "contextRail.configure.dialogDescription": "Elige qué paneles muestra la barra. Los paneles ocultos conservan sus datos y siguen accesibles desde la paleta de comandos.", + "contextRail.configure.showAll": "Mostrar todos", + "contextRail.configure.noneWarning": "Todos los paneles están ocultos.", "contextRail.aria.rail": "Superficies del panel", "contextPanel.editorEmpty.title": "Ningún archivo abierto", "contextPanel.editorEmpty.description": "Elige un archivo del árbol para empezar a editar.", diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index 34528808..b35feb84 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -959,6 +959,11 @@ export const dict = { 'contextPanel.mode.context': 'Contexte', 'contextPanel.mode.preview': 'Aperçu', 'contextPanel.mode.browser': 'Navigateur', + 'contextRail.configure.open': 'Configurer les panneaux', + 'contextRail.configure.dialogTitle': 'Panneaux de la barre', + 'contextRail.configure.dialogDescription': 'Choisissez les panneaux affichés par la barre. Les panneaux masqués conservent leurs données et restent accessibles via la palette de commandes.', + 'contextRail.configure.showAll': 'Tout afficher', + 'contextRail.configure.noneWarning': 'Tous les panneaux sont masqués.', 'contextRail.aria.rail': 'Surfaces du panneau', 'contextPanel.editorEmpty.title': 'Aucun fichier ouvert', 'contextPanel.editorEmpty.description': 'Choisissez un fichier dans l’arborescence pour commencer.', diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts index daedcf79..1eeb39e0 100644 --- a/packages/ui/src/lib/i18n/messages/ja.ts +++ b/packages/ui/src/lib/i18n/messages/ja.ts @@ -1136,6 +1136,11 @@ export const dict: Record = { 'contextPanel.mode.context': 'コンテキスト', 'contextPanel.mode.preview': 'プレビュー', 'contextPanel.mode.browser': 'ブラウザ', + 'contextRail.configure.open': 'パネルを設定', + 'contextRail.configure.dialogTitle': 'レールのパネル', + 'contextRail.configure.dialogDescription': 'レールに表示するパネルを選択します。非表示のパネルもデータは保持され、コマンドパレットから引き続き開けます。', + 'contextRail.configure.showAll': 'すべて表示', + 'contextRail.configure.noneWarning': 'すべてのパネルが非表示です。', 'contextRail.aria.rail': 'パネルサーフェス', 'contextPanel.editorEmpty.title': 'ファイルが開かれていません', 'contextPanel.editorEmpty.description': 'ツリーからファイルを選んで編集を始めましょう。', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index da927fe9..b2c9192a 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -1140,6 +1140,11 @@ export const dict: Record = { 'contextPanel.mode.context': '컨텍스트', 'contextPanel.mode.preview': '미리보기', 'contextPanel.mode.browser': '브라우저', + 'contextRail.configure.open': '패널 구성', + 'contextRail.configure.dialogTitle': '레일 패널', + 'contextRail.configure.dialogDescription': '레일에 표시할 패널을 선택하세요. 숨긴 패널의 데이터는 유지되며 명령 팔레트에서 계속 열 수 있습니다.', + 'contextRail.configure.showAll': '모두 표시', + 'contextRail.configure.noneWarning': '모든 패널이 숨겨져 있습니다.', 'contextRail.aria.rail': '패널 서피스', 'contextPanel.editorEmpty.title': '열린 파일 없음', 'contextPanel.editorEmpty.description': '트리에서 파일을 선택해 편집을 시작하세요.', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index dcd5e758..25933ab1 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -1478,6 +1478,11 @@ export const dict: Record = { 'contextPanel.mode.pr': 'Pull Request', 'contextPanel.mode.preview': 'Podgląd', 'contextPanel.mode.browser': 'Przeglądarka', + 'contextRail.configure.open': 'Konfiguruj panele', + 'contextRail.configure.dialogTitle': 'Panele paska', + 'contextRail.configure.dialogDescription': 'Wybierz, które panele pokazuje pasek. Ukryte panele zachowują dane i pozostają dostępne z palety poleceń.', + 'contextRail.configure.showAll': 'Pokaż wszystkie', + 'contextRail.configure.noneWarning': 'Wszystkie panele są ukryte.', 'contextRail.aria.rail': 'Powierzchnie panelu', 'contextPanel.editorEmpty.title': 'Brak otwartego pliku', 'contextPanel.editorEmpty.description': 'Wybierz plik z drzewa, aby rozpocząć edycję.', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index 4da1628e..932b3ea3 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -1140,6 +1140,11 @@ export const dict: Record = { "contextPanel.mode.context": "Contexto", "contextPanel.mode.preview": "Prévia", "contextPanel.mode.browser": "Navegador", + "contextRail.configure.open": "Configurar painéis", + "contextRail.configure.dialogTitle": "Painéis da barra", + "contextRail.configure.dialogDescription": "Escolha quais painéis a barra mostra. Painéis ocultos mantêm seus dados e continuam acessíveis pela paleta de comandos.", + "contextRail.configure.showAll": "Mostrar todos", + "contextRail.configure.noneWarning": "Todos os painéis estão ocultos.", "contextRail.aria.rail": "Superfícies do painel", "contextPanel.editorEmpty.title": "Nenhum arquivo aberto", "contextPanel.editorEmpty.description": "Escolha um arquivo na árvore para começar a editar.", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index f7c6adcd..56f101b1 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -1140,6 +1140,11 @@ export const dict: Record = { "contextPanel.mode.context": "Контекст", "contextPanel.mode.preview": "Перегляд", "contextPanel.mode.browser": "Браузер", + "contextRail.configure.open": "Налаштувати панелі", + "contextRail.configure.dialogTitle": "Панелі рейки", + "contextRail.configure.dialogDescription": "Обери, які панелі показує рейка. Приховані панелі зберігають дані й доступні з палітри команд.", + "contextRail.configure.showAll": "Показати всі", + "contextRail.configure.noneWarning": "Усі панелі приховано.", "contextRail.aria.rail": "Поверхні панелі", "contextPanel.editorEmpty.title": "Файл не відкрито", "contextPanel.editorEmpty.description": "Виберіть файл у дереві, щоб почати редагування.", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index 267b2ab7..2616b032 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -1140,6 +1140,11 @@ export const dict: Record = { 'contextPanel.mode.context': '上下文', 'contextPanel.mode.preview': '预览', 'contextPanel.mode.browser': '浏览器', + 'contextRail.configure.open': '配置面板', + 'contextRail.configure.dialogTitle': '侧栏面板', + 'contextRail.configure.dialogDescription': '选择侧栏显示哪些面板。隐藏的面板会保留数据,仍可通过命令面板打开。', + 'contextRail.configure.showAll': '全部显示', + 'contextRail.configure.noneWarning': '所有面板均已隐藏。', 'contextRail.aria.rail': '面板界面', 'contextPanel.editorEmpty.title': '未打开文件', 'contextPanel.editorEmpty.description': '从文件树中选择一个文件开始编辑。', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index f392a509..4dc07807 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -1152,6 +1152,11 @@ export const dict: Record = { 'contextPanel.mode.context': '上下文', 'contextPanel.mode.preview': '預覽', 'contextPanel.mode.browser': '瀏覽器', + 'contextRail.configure.open': '設定面板', + 'contextRail.configure.dialogTitle': '側欄面板', + 'contextRail.configure.dialogDescription': '選擇側欄顯示哪些面板。隱藏的面板會保留資料,仍可透過命令面板開啟。', + 'contextRail.configure.showAll': '全部顯示', + 'contextRail.configure.noneWarning': '所有面板皆已隱藏。', 'contextRail.aria.rail': '面板介面', 'contextPanel.editorEmpty.title': '未開啟檔案', 'contextPanel.editorEmpty.description': '從檔案樹選擇檔案開始編輯。', diff --git a/packages/ui/src/lib/surfaces/DOCUMENTATION.md b/packages/ui/src/lib/surfaces/DOCUMENTATION.md index 9a584ce5..fd750557 100644 --- a/packages/ui/src/lib/surfaces/DOCUMENTATION.md +++ b/packages/ui/src/lib/surfaces/DOCUMENTATION.md @@ -22,7 +22,10 @@ edge (`components/layout/ContextPanelRail.tsx`) and rendered by registry's default order and appends any missing surfaces. - `getVisibleContextRailSurfaces` is the single visibility filter shared by the rail and the global surface-switch shortcut (`switch_context_surface` in - `lib/shortcuts.ts`): it drops the plan surface unless plan mode is enabled, + `lib/shortcuts`): it drops surfaces the user hid + (`useUIStore.contextRailHiddenSurfaces`, edited from the rail's trailing + configure button — `ContextRailSurfacesDialog`), drops the plan surface + unless plan mode is enabled, drops the walkthrough on VS Code and below `WALKTHROUGH_MIN_WIDTH`, and hides `has-content` surfaces until a tab of their mode exists. Both consumers use it so the digit shown on a rail badge always maps to the same surface the diff --git a/packages/ui/src/lib/surfaces/registry.ts b/packages/ui/src/lib/surfaces/registry.ts index 730389e8..21d2992f 100644 --- a/packages/ui/src/lib/surfaces/registry.ts +++ b/packages/ui/src/lib/surfaces/registry.ts @@ -187,6 +187,9 @@ export const sortContextSurfaces = (railOrder: readonly string[]): ContextSurfac type VisibleRailSurfacesOptions = { railOrder: readonly string[]; + /** Surfaces the user chose to hide from the rail (and from the digit + shortcuts, which share this filter). */ + hiddenSurfaces?: readonly string[]; planModeEnabled: boolean; isVSCode: boolean; screenWidth: number; @@ -203,6 +206,9 @@ type VisibleRailSurfacesOptions = { */ export const getVisibleContextRailSurfaces = (options: VisibleRailSurfacesOptions): ContextSurfaceDescriptor[] => { return sortContextSurfaces(options.railOrder).filter((surface) => { + if (options.hiddenSurfaces?.includes(surface.id)) { + return false; + } if (surface.id === 'plan' && !options.planModeEnabled) { return false; } diff --git a/packages/ui/src/stores/useUIStore.ts b/packages/ui/src/stores/useUIStore.ts index c377f27b..b5254f2e 100644 --- a/packages/ui/src/stores/useUIStore.ts +++ b/packages/ui/src/stores/useUIStore.ts @@ -607,6 +607,9 @@ interface UIStore { hasManuallyResizedLeftSidebar: boolean; contextPanelByDirectory: Record; contextRailOrder: string[]; + /** Surface ids the user hid from the context rail; stored as the hidden set + so surfaces added later appear for everyone. */ + contextRailHiddenSurfaces: string[]; contextEditorTreeVisible: boolean; contextEditorTreeWidth: number; notesPanelHeight: number; @@ -828,6 +831,8 @@ interface UIStore { setWorkStatusOverlayOpen: (open: boolean) => void; setWorkStatusSectionVisible: (sectionId: string, visible: boolean) => void; setWorkStatusHiddenSections: (sectionIds: string[]) => void; + setContextRailSurfaceVisible: (surfaceId: string, visible: boolean) => void; + setContextRailHiddenSurfaces: (surfaceIds: string[]) => void; setSessionSwitcherOpen: (open: boolean) => void; setSessionDropdownOpen: (open: boolean) => void; setPendingDiffFile: (filePath: string | null, staged?: boolean, scope?: PendingDiffScope | null) => void; @@ -990,6 +995,7 @@ export const useUIStore = create()( hasManuallyResizedLeftSidebar: false, contextPanelByDirectory: {}, contextRailOrder: [], + contextRailHiddenSurfaces: [], contextEditorTreeVisible: true, contextEditorTreeWidth: 240, notesPanelHeight: 112, @@ -1599,6 +1605,23 @@ export const useUIStore = create()( set({ workStatusHiddenSections: [...new Set(sectionIds)] }); }, + setContextRailSurfaceVisible: (surfaceId, visible) => { + set((state) => { + const hidden = state.contextRailHiddenSurfaces; + const isHidden = hidden.includes(surfaceId); + if (visible === !isHidden) return state; + return { + contextRailHiddenSurfaces: visible + ? hidden.filter((entry) => entry !== surfaceId) + : [...hidden, surfaceId], + }; + }); + }, + + setContextRailHiddenSurfaces: (surfaceIds) => { + set({ contextRailHiddenSurfaces: [...new Set(surfaceIds)] }); + }, + setSessionSwitcherOpen: (open) => { if (get().isSessionSwitcherOpen === open) { @@ -2627,6 +2650,9 @@ export const useUIStore = create()( state.autoSaveEnabled = true; } + state.contextRailHiddenSurfaces = Array.isArray(state.contextRailHiddenSurfaces) + ? (state.contextRailHiddenSurfaces as unknown[]).filter((id): id is string => typeof id === 'string' && id.trim() !== '') + : []; state.contextRailOrder = Array.isArray(state.contextRailOrder) ? (state.contextRailOrder as unknown[]).filter((id): id is string => typeof id === 'string' && id.trim() !== '') : []; @@ -2639,6 +2665,7 @@ export const useUIStore = create()( sidebarWidth: state.sidebarWidth, contextPanelByDirectory: state.contextPanelByDirectory, contextRailOrder: state.contextRailOrder, + contextRailHiddenSurfaces: state.contextRailHiddenSurfaces, contextEditorTreeVisible: state.contextEditorTreeVisible, contextEditorTreeWidth: state.contextEditorTreeWidth, notesPanelHeight: state.notesPanelHeight,