From 7db78b795d83a5e557c95f6b05c6d5624154de00 Mon Sep 17 00:00:00 2001 From: Adam Dalloul <47503782+Adam-Dalloul@users.noreply.github.com> Date: Fri, 21 Aug 2026 13:10:47 -0700 Subject: [PATCH] feat(settings): let the sidebar hide Automations, To-dos, and Repository panel A new Sidebar navigation section in Settings > Appearance carries one switch per fixed nav row. Every row defaults to visible, New chat is deliberately not hideable, and hidden pages stay reachable from the status bar's quick actions menu. The preference is a single localStorage record synced across windows like the other appearance settings; hidden rows unmount rather than hide with CSS. --- src/components/appearance-provider.test.tsx | 89 ++++++++++++++- src/components/appearance-provider.tsx | 41 +++++++ src/components/layout/sidebar.test.tsx | 48 +++++++++ src/components/layout/sidebar.tsx | 101 ++++++++++-------- .../settings/appearance-settings.tsx | 51 ++++++++- src/hooks/use-appearance.ts | 7 ++ src/i18n/messages/ar.json | 7 ++ src/i18n/messages/de.json | 7 ++ src/i18n/messages/en.json | 7 ++ src/i18n/messages/es.json | 7 ++ src/i18n/messages/fr.json | 7 ++ src/i18n/messages/ja.json | 7 ++ src/i18n/messages/ko.json | 7 ++ src/i18n/messages/pt.json | 7 ++ src/i18n/messages/zh-CN.json | 7 ++ src/i18n/messages/zh-TW.json | 7 ++ src/lib/appearance-script.ts | 7 ++ src/lib/sidebar-nav-visibility.test.ts | 67 ++++++++++++ src/lib/sidebar-nav-visibility.ts | 51 +++++++++ 19 files changed, 484 insertions(+), 48 deletions(-) create mode 100644 src/lib/sidebar-nav-visibility.test.ts create mode 100644 src/lib/sidebar-nav-visibility.ts diff --git a/src/components/appearance-provider.test.tsx b/src/components/appearance-provider.test.tsx index 3aa816288..8de2d5481 100644 --- a/src/components/appearance-provider.test.tsx +++ b/src/components/appearance-provider.test.tsx @@ -2,8 +2,11 @@ import { act, fireEvent, render, screen } from "@testing-library/react" import { afterEach, beforeEach, describe, expect, it, vi } from "vitest" import { AppearanceProvider } from "./appearance-provider" -import { useCustomStyle } from "@/hooks/use-appearance" -import { STORAGE_KEY_CUSTOM_THEME } from "@/lib/appearance-script" +import { useCustomStyle, useSidebarNavVisibility } from "@/hooks/use-appearance" +import { + STORAGE_KEY_CUSTOM_THEME, + STORAGE_KEY_SIDEBAR_NAV_VISIBILITY, +} from "@/lib/appearance-script" function Probe() { const { setCustomThemeToken } = useCustomStyle() @@ -93,3 +96,85 @@ describe("debounced persistence", () => { expect(storedPrimary()).toBe("#bbbbbb") }) }) + +function NavVisibilityProbe() { + const { sidebarNavVisibility, setSidebarNavItemVisible } = + useSidebarNavVisibility() + return ( + <> + + {JSON.stringify(sidebarNavVisibility)} + + setSidebarNavItemVisible("forge", false)}> + hide-forge + + > + ) +} + +describe("sidebar nav visibility", () => { + const renderNavProbe = () => + render( + + + + ) + + const visibility = () => + JSON.parse(screen.getByTestId("nav-visibility").textContent ?? "") + + it("defaults every row to visible and round-trips a hide through storage", () => { + const { unmount } = renderNavProbe() + expect(visibility()).toEqual({ + automations: true, + tasks: true, + forge: true, + }) + + fireEvent.click(screen.getByText("hide-forge")) + expect(visibility()).toEqual({ + automations: true, + tasks: true, + forge: false, + }) + // Persisted immediately (a discrete toggle, so no debounce to wait out). + expect( + JSON.parse(localStorage.getItem(STORAGE_KEY_SIDEBAR_NAV_VISIBILITY) ?? "") + ).toEqual({ automations: true, tasks: true, forge: false }) + + // A fresh mount reads the stored record back: the mount effect reconciles + // the SSR-safe all-visible initial state with the persisted choice. + unmount() + renderNavProbe() + expect(visibility()).toEqual({ + automations: true, + tasks: true, + forge: false, + }) + }) + + it("follows a change written by another window via the storage event", () => { + // Settings is its own window — without this sync a row hidden there would + // only disappear from the workspace after a reload. + renderNavProbe() + const next = JSON.stringify({ + automations: false, + tasks: true, + forge: true, + }) + act(() => { + localStorage.setItem(STORAGE_KEY_SIDEBAR_NAV_VISIBILITY, next) + window.dispatchEvent( + new StorageEvent("storage", { + key: STORAGE_KEY_SIDEBAR_NAV_VISIBILITY, + newValue: next, + }) + ) + }) + expect(visibility()).toEqual({ + automations: false, + tasks: true, + forge: true, + }) + }) +}) diff --git a/src/components/appearance-provider.tsx b/src/components/appearance-provider.tsx index 4cdebc977..fe498e002 100644 --- a/src/components/appearance-provider.tsx +++ b/src/components/appearance-provider.tsx @@ -31,6 +31,7 @@ import { STORAGE_KEY_THEME_COLOR, STORAGE_KEY_ZOOM_LEVEL, STORAGE_KEY_WELCOME_QUICK_ACTIONS, + STORAGE_KEY_SIDEBAR_NAV_VISIBILITY, STORAGE_KEY_UI_FONT, STORAGE_KEY_UI_FONT_CUSTOM, STORAGE_KEY_UI_FONT_STACK, @@ -66,6 +67,12 @@ import { } from "@/lib/custom-style" import { useShortcutSettings } from "@/hooks/use-shortcut-settings" import { matchShortcutEvent } from "@/lib/keyboard-shortcuts" +import { + DEFAULT_SIDEBAR_NAV_VISIBILITY, + parseSidebarNavVisibility, + type HideableSidebarNavId, + type SidebarNavVisibility, +} from "@/lib/sidebar-nav-visibility" import { DEFAULT_WORKSPACE_BG_ENABLED, DEFAULT_WORKSPACE_BG_MASK_OPACITY, @@ -110,6 +117,10 @@ type AppearanceContextValue = { /** 新会话欢迎页是否显示「模式选择区域」(QuickActions 快捷卡片),默认开启 */ showWelcomeQuickActions: boolean setShowWelcomeQuickActions: (on: boolean) => void + /** Which of the sidebar's fixed nav rows (Automations / To-dos / Repository + * panel) are shown. Every row defaults to visible. */ + sidebarNavVisibility: SidebarNavVisibility + setSidebarNavItemVisible: (id: HideableSidebarNavId, visible: boolean) => void /** 界面字体(普通组件,驱动 --font-sans) */ uiFont: FontSelection setUiFont: (id: string, custom?: string) => void @@ -356,6 +367,20 @@ export function AppearanceProvider({ const [showWelcomeQuickActions, setShowWelcomeQuickActionsState] = useState(() => readBool(STORAGE_KEY_WELCOME_QUICK_ACTIONS, true)) + // Sidebar fixed-nav visibility starts at the all-visible default and picks + // up the stored record in a mount effect below. Unlike the welcome quick + // actions (client-only), the nav rows are part of the server-rendered + // markup, so reading localStorage in the initializer would desync SSR/CSR + // markup for anyone who hid a row. + const [sidebarNavVisibility, setSidebarNavVisibilityState] = + useState(DEFAULT_SIDEBAR_NAV_VISIBILITY) + + useEffect(() => { + setSidebarNavVisibilityState( + parseSidebarNavVisibility(readStored(STORAGE_KEY_SIDEBAR_NAV_VISIBILITY)) + ) + }, []) + // 字体偏好的初始值从 localStorage 读 id/custom(视觉已由 inline 脚本就位, // 这里只是回填选中态,不会造成闪烁)。 const [uiFont, setUiFontState] = useState(() => @@ -478,6 +503,15 @@ export function AppearanceProvider({ persist(STORAGE_KEY_WELCOME_QUICK_ACTIONS, on ? "1" : "0") }, []) + const setSidebarNavItemVisible = useCallback( + (id: HideableSidebarNavId, visible: boolean) => { + const next = { ...sidebarNavVisibility, [id]: visible } + setSidebarNavVisibilityState(next) + persist(STORAGE_KEY_SIDEBAR_NAV_VISIBILITY, JSON.stringify(next)) + }, + [sidebarNavVisibility] + ) + const setUiFont = useCallback((id: string, custom = "") => { setUiFontState({ id, custom }) const stack = resolveFontStack(id, custom, "sans") @@ -829,6 +863,11 @@ export function AppearanceProvider({ readBool(STORAGE_KEY_WELCOME_QUICK_ACTIONS, true) ) } + // Settings is its own window: a nav row hidden there reaches the + // workspace window through this event. Removal (null) = defaults. + if (e.key === STORAGE_KEY_SIDEBAR_NAV_VISIBILITY) { + setSidebarNavVisibilityState(parseSidebarNavVisibility(e.newValue)) + } if (e.key && FONT_KEYS.has(e.key)) { rehydrateFonts() } @@ -927,6 +966,8 @@ export function AppearanceProvider({ setZoomLevel, showWelcomeQuickActions, setShowWelcomeQuickActions, + sidebarNavVisibility, + setSidebarNavItemVisible, uiFont, setUiFont, editorFont, diff --git a/src/components/layout/sidebar.test.tsx b/src/components/layout/sidebar.test.tsx index 751fafe1c..abc8df1f4 100644 --- a/src/components/layout/sidebar.test.tsx +++ b/src/components/layout/sidebar.test.tsx @@ -24,6 +24,8 @@ const spies = vi.hoisted(() => ({ })) const mockState = vi.hoisted(() => ({ activeFolder: { id: 7, path: "/x" } as { id: number; path: string } | null, + // Settings → Appearance visibility of the fixed nav rows (default: all on). + navVisibility: { automations: true, tasks: true, forge: true }, })) // The conversation list is irrelevant here — stub it so the test exercises only @@ -82,6 +84,10 @@ vi.mock("@/hooks/use-shortcut-settings", () => ({ vi.mock("@/hooks/use-mobile", () => ({ useIsMobile: () => false })) vi.mock("@/hooks/use-appearance", () => ({ useZoomLevel: () => ({ zoomLevel: 100, setZoomLevel: () => {} }), + useSidebarNavVisibility: () => ({ + sidebarNavVisibility: mockState.navVisibility, + setSidebarNavItemVisible: vi.fn(), + }), })) function renderSidebar() { @@ -147,6 +153,48 @@ describe("Sidebar — fixed nav region", () => { }) }) +describe("Sidebar — fixed nav visibility (Settings → Appearance)", () => { + beforeEach(() => { + mockState.navVisibility = { automations: true, tasks: true, forge: true } + mockState.activeFolder = { id: 7, path: "/x" } + }) + + it("shows every route row by default", () => { + const { getByText } = renderSidebar() + expect(getByText("Automations")).toBeTruthy() + expect(getByText("To-dos")).toBeTruthy() + expect(getByText("Repository panel")).toBeTruthy() + }) + + it("unmounts a hidden row entirely, leaving its neighbours alone", () => { + mockState.navVisibility = { automations: false, tasks: true, forge: true } + const { getByText, queryByText } = renderSidebar() + // Fully absent from the DOM — not CSS-hidden, so nothing focusable or + // announceable is left behind. + expect(queryByText("Automations")).toBeNull() + expect(getByText("To-dos")).toBeTruthy() + expect(getByText("Repository panel")).toBeTruthy() + }) + + it("hides the Repository panel row together with its Beta badge", () => { + mockState.navVisibility = { automations: true, tasks: true, forge: false } + const { queryByText } = renderSidebar() + expect(queryByText("Repository panel")).toBeNull() + expect(queryByText("Beta")).toBeNull() + }) + + it("keeps New chat even with every hideable row hidden", () => { + mockState.navVisibility = { automations: false, tasks: false, forge: false } + const { getByText, queryByText } = renderSidebar() + // New chat is deliberately not hideable — the primary entry point into the + // workspace survives any combination of the toggles. + expect(getByText("New chat")).toBeTruthy() + expect(queryByText("Automations")).toBeNull() + expect(queryByText("To-dos")).toBeNull() + expect(queryByText("Repository panel")).toBeNull() + }) +}) + describe("Sidebar — Show worktree folders toggle", () => { beforeEach(() => { localStorage.clear() diff --git a/src/components/layout/sidebar.tsx b/src/components/layout/sidebar.tsx index e7e63ee24..c4353ea59 100644 --- a/src/components/layout/sidebar.tsx +++ b/src/components/layout/sidebar.tsx @@ -39,7 +39,7 @@ import { import { useIsMobile } from "@/hooks/use-mobile" import { useIsMac } from "@/hooks/use-is-mac" import { usePlatform } from "@/hooks/use-platform" -import { useZoomLevel } from "@/hooks/use-appearance" +import { useSidebarNavVisibility, useZoomLevel } from "@/hooks/use-appearance" import { useShortcutSettings } from "@/hooks/use-shortcut-settings" import { formatShortcutLabel } from "@/lib/keyboard-shortcuts" import { isDesktop } from "@/lib/platform" @@ -138,6 +138,11 @@ export function Sidebar() { const isMac = useIsMac() const { isMac: platformIsMac } = usePlatform() const { zoomLevel } = useZoomLevel() + // Settings → Appearance controls which route rows render below. New chat is + // exempt on purpose: it is the primary entry point and never hides. Hidden + // routes stay reachable from the status bar's quick-actions menu, which + // exists precisely to be the always-on path to every workbench route. + const { sidebarNavVisibility } = useSidebarNavVisibility() const { shortcuts } = useShortcutSettings() const isMobile = useIsMobile() const listRef = useRef(null) @@ -447,50 +452,56 @@ export function Sidebar() { {/* Both route rows close the mobile Sheet on the way out, like tapping a conversation card (handled by the list wrapper below) — otherwise the page they just opened stays hidden behind the sidebar. */} - { - if (isMobile) toggle() - setRoute("automations") - }} - trailing={ - unseenFailures > 0 ? ( - - {unseenFailures} - - ) : null - } - /> - { - if (isMobile) toggle() - setRoute("tasks") - }} - trailing={ - attentionCount > 0 ? ( - // Attention (not failure): tasks waiting on the user — primary - // tint like the shortcut chips, not destructive. - - {attentionCount} - - ) : null - } - /> - { - if (isMobile) toggle() - setRoute("forge") - }} - trailing={} - /> + {sidebarNavVisibility.automations && ( + { + if (isMobile) toggle() + setRoute("automations") + }} + trailing={ + unseenFailures > 0 ? ( + + {unseenFailures} + + ) : null + } + /> + )} + {sidebarNavVisibility.tasks && ( + { + if (isMobile) toggle() + setRoute("tasks") + }} + trailing={ + attentionCount > 0 ? ( + // Attention (not failure): tasks waiting on the user — primary + // tint like the shortcut chips, not destructive. + + {attentionCount} + + ) : null + } + /> + )} + {sidebarNavVisibility.forge && ( + { + if (isMobile) toggle() + setRoute("forge") + }} + trailing={} + /> + )} {/* On mobile, clicking a conversation card auto-closes the Sheet */} diff --git a/src/components/settings/appearance-settings.tsx b/src/components/settings/appearance-settings.tsx index f09e69636..5397197f1 100644 --- a/src/components/settings/appearance-settings.tsx +++ b/src/components/settings/appearance-settings.tsx @@ -1,6 +1,6 @@ "use client" -import { LayoutGrid, Monitor, Moon, Sun, Type } from "lucide-react" +import { LayoutGrid, Monitor, Moon, PanelLeft, Sun, Type } from "lucide-react" import { useTranslations } from "next-intl" import { useTheme } from "next-themes" import { ScrollArea } from "@/components/ui/scroll-area" @@ -13,6 +13,7 @@ import { } from "@/components/ui/select" import { Switch } from "@/components/ui/switch" import { + useSidebarNavVisibility, useThemeColor, useZoomLevel, useWelcomeQuickActions, @@ -40,6 +41,8 @@ export function AppearanceSettings() { const { zoomLevel, setZoomLevel } = useZoomLevel() const { showWelcomeQuickActions, setShowWelcomeQuickActions } = useWelcomeQuickActions() + const { sidebarNavVisibility, setSidebarNavItemVisible } = + useSidebarNavVisibility() const resolvedThemeLabel = resolvedTheme === "dark" @@ -238,6 +241,52 @@ export function AppearanceSettings() { + {/* ===== Sidebar — fixed navigation entries ===== */} + + + + + {t("sidebarNav.sectionTitle")} + + + + + {t("sidebarNav.sectionDescription")} + + + + + + setSidebarNavItemVisible("automations", on) + } + /> + + {t("sidebarNav.showAutomations")} + + + + setSidebarNavItemVisible("tasks", on)} + /> + + {t("sidebarNav.showTasks")} + + + + setSidebarNavItemVisible("forge", on)} + /> + + {t("sidebarNav.showForge")} + + + + + {/* ===== Desktop Pet ===== */} diff --git a/src/hooks/use-appearance.ts b/src/hooks/use-appearance.ts index 6a93f457a..f0926917f 100644 --- a/src/hooks/use-appearance.ts +++ b/src/hooks/use-appearance.ts @@ -31,6 +31,13 @@ export function useWelcomeQuickActions() { return { showWelcomeQuickActions, setShowWelcomeQuickActions } } +/** Semantic wrapper: which of the sidebar's fixed nav rows (Automations / + * To-dos / Repository panel) are shown. */ +export function useSidebarNavVisibility() { + const { sidebarNavVisibility, setSidebarNavItemVisible } = useAppearance() + return { sidebarNavVisibility, setSidebarNavItemVisible } +} + /** 界面字体(普通组件)。stack 已解析,可直接用于 style 或 CSS 变量。 */ export function useUiFont() { const { uiFont, setUiFont } = useAppearance() diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 71b3107cb..14f4d013a 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -173,6 +173,13 @@ "sectionDescription": "بطاقات الاختصار «تطوير الكود / الأعمال المكتبية» التي تظهر أعلى مربع الإدخال في صفحة المحادثة الجديدة.", "showQuickActions": "العرض في صفحة المحادثة الجديدة" }, + "sidebarNav": { + "sectionTitle": "التنقل في الشريط الجانبي", + "sectionDescription": "اختر الإدخالات الثابتة التي يعرضها الشريط الجانبي أعلى قائمة المحادثات. تظل الصفحات المخفية متاحة من قائمة الإجراءات السريعة في شريط الحالة.", + "showAutomations": "عرض الأتمتة", + "showTasks": "عرض المهام قيد الانتظار", + "showForge": "عرض لوحة المستودع" + }, "workspaceBackground": { "sectionTitle": "خلفية مساحة العمل", "sectionDescription": "عرض صورة خلف مساحة العمل بالكامل. يصبح الشريط الجانبي واللوحات شبه شفافة ومصنفرة لتظهر الصورة من خلالها، ويحافظ القناع على وضوح النص.", diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index a2275d05e..21578fc1b 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -173,6 +173,13 @@ "sectionDescription": "Die Shortcut-Karten „Code-Entwicklung / Büroarbeit“ über dem Eingabefeld auf der Seite für eine neue Konversation.", "showQuickActions": "Auf der Seite für neue Konversationen anzeigen" }, + "sidebarNav": { + "sectionTitle": "Seitenleisten-Navigation", + "sectionDescription": "Wähle, welche festen Einträge die Seitenleiste über der Konversationsliste anzeigt. Ausgeblendete Seiten bleiben über das Schnellaktionen-Menü in der Statusleiste erreichbar.", + "showAutomations": "Automatisierungen anzeigen", + "showTasks": "To-dos anzeigen", + "showForge": "Repository-Panel anzeigen" + }, "workspaceBackground": { "sectionTitle": "Arbeitsbereich-Hintergrund", "sectionDescription": "Zeigt ein Bild hinter dem gesamten Arbeitsbereich. Seitenleiste und Panels werden durchscheinend und mattiert, sodass das Bild durchscheint, und eine Maske sorgt für lesbaren Text.", diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 1bb45ba19..c86c31382 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -173,6 +173,13 @@ "sectionDescription": "The Code Development / Office Work shortcut cards shown above the composer on the new conversation page.", "showQuickActions": "Show on the new conversation page" }, + "sidebarNav": { + "sectionTitle": "Sidebar navigation", + "sectionDescription": "Choose which fixed entries the sidebar shows above the conversation list. Hidden pages stay reachable from the quick actions menu in the status bar.", + "showAutomations": "Show Automations", + "showTasks": "Show To-dos", + "showForge": "Show Repository panel" + }, "workspaceBackground": { "sectionTitle": "Workspace background", "sectionDescription": "Show a picture behind the whole workspace. Sidebar and panels turn translucent and frosted so the image shows through, and a mask keeps text readable.", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 8bdd326d9..3344ed5d4 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -173,6 +173,13 @@ "sectionDescription": "Las tarjetas de acceso rápido «Desarrollo de código / Ofimática» que se muestran sobre el editor en la página de nueva conversación.", "showQuickActions": "Mostrar en la página de nueva conversación" }, + "sidebarNav": { + "sectionTitle": "Navegación de la barra lateral", + "sectionDescription": "Elige qué entradas fijas muestra la barra lateral encima de la lista de conversaciones. Las páginas ocultas siguen disponibles en el menú de acciones rápidas de la barra de estado.", + "showAutomations": "Mostrar Automatizaciones", + "showTasks": "Mostrar Tareas pendientes", + "showForge": "Mostrar Panel del repositorio" + }, "workspaceBackground": { "sectionTitle": "Fondo del espacio de trabajo", "sectionDescription": "Muestra una imagen detrás de todo el espacio de trabajo. La barra lateral y los paneles se vuelven translúcidos y esmerilados para dejar ver la imagen, y una máscara mantiene el texto legible.", diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index aefe367ec..6f5b5add1 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -173,6 +173,13 @@ "sectionDescription": "Les cartes de raccourci « Développement / Bureautique » affichées au-dessus du champ de saisie sur la page de nouvelle conversation.", "showQuickActions": "Afficher sur la page de nouvelle conversation" }, + "sidebarNav": { + "sectionTitle": "Navigation de la barre latérale", + "sectionDescription": "Choisissez les entrées fixes que la barre latérale affiche au-dessus de la liste des conversations. Les pages masquées restent accessibles depuis le menu Actions rapides de la barre d'état.", + "showAutomations": "Afficher les automatisations", + "showTasks": "Afficher les tâches à faire", + "showForge": "Afficher le panneau du dépôt" + }, "workspaceBackground": { "sectionTitle": "Arrière-plan de l'espace de travail", "sectionDescription": "Affiche une image derrière tout l'espace de travail. La barre latérale et les panneaux deviennent translucides et dépolis pour laisser transparaître l'image, et un masque préserve la lisibilité du texte.", diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 7eb01e626..563fb4ee4 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -173,6 +173,13 @@ "sectionDescription": "新しい会話ページで入力欄の上に表示される「コード開発 / 日常業務」のショートカットカードです。", "showQuickActions": "新しい会話ページに表示する" }, + "sidebarNav": { + "sectionTitle": "サイドバーのナビゲーション", + "sectionDescription": "会話リストの上に表示する固定エントリを選びます。非表示にしたページもステータスバーの「クイック操作」メニューから開けます。", + "showAutomations": "オートメーションを表示", + "showTasks": "ToDo タスクを表示", + "showForge": "リポジトリパネルを表示" + }, "workspaceBackground": { "sectionTitle": "ワークスペースの背景", "sectionDescription": "ワークスペース全体の背後に画像を表示します。サイドバーやパネルは半透明のすりガラスになり画像が透けて見えます。マスクで文字の可読性を保ちます。", diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index b6de08a7d..4c8cd69e0 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -173,6 +173,13 @@ "sectionDescription": "새 대화 페이지에서 입력창 위에 표시되는 '코드 개발 / 일상 업무' 바로가기 카드입니다.", "showQuickActions": "새 대화 페이지에 표시" }, + "sidebarNav": { + "sectionTitle": "사이드바 내비게이션", + "sectionDescription": "대화 목록 위에 표시할 고정 항목을 선택합니다. 숨긴 페이지는 상태 표시줄의 '빠른 작업' 메뉴에서 계속 열 수 있습니다.", + "showAutomations": "자동화 표시", + "showTasks": "할 일 표시", + "showForge": "리포지토리 패널 표시" + }, "workspaceBackground": { "sectionTitle": "작업 공간 배경", "sectionDescription": "작업 공간 전체 뒤에 이미지를 표시합니다. 사이드바와 패널이 반투명 유리 효과로 바뀌어 이미지가 비쳐 보이며, 마스크가 텍스트 가독성을 유지합니다.", diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 913bf206b..746d6be73 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -173,6 +173,13 @@ "sectionDescription": "Os cartões de atalho «Desenvolvimento / Escritório» exibidos acima do campo de entrada na página de nova conversa.", "showQuickActions": "Mostrar na página de nova conversa" }, + "sidebarNav": { + "sectionTitle": "Navegação da barra lateral", + "sectionDescription": "Escolha quais entradas fixas a barra lateral mostra acima da lista de conversas. As páginas ocultas continuam acessíveis pelo menu de ações rápidas na barra de status.", + "showAutomations": "Mostrar Automações", + "showTasks": "Mostrar Tarefas a fazer", + "showForge": "Mostrar Painel do repositório" + }, "workspaceBackground": { "sectionTitle": "Plano de fundo da área de trabalho", "sectionDescription": "Mostra uma imagem atrás de toda a área de trabalho. A barra lateral e os painéis ficam translúcidos e foscos para deixar a imagem transparecer, e uma máscara mantém o texto legível.", diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index a5cceb512..9d6e947d2 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -173,6 +173,13 @@ "sectionDescription": "新会话页面输入框上方显示的「代码开发 / 日常办公」快捷卡片区域。", "showQuickActions": "在新会话页面显示" }, + "sidebarNav": { + "sectionTitle": "侧边栏导航", + "sectionDescription": "选择侧边栏在会话列表上方显示哪些固定入口。隐藏的页面仍可通过状态栏的「快捷操作」菜单打开。", + "showAutomations": "显示自动化", + "showTasks": "显示待办任务", + "showForge": "显示仓库面板" + }, "workspaceBackground": { "sectionTitle": "工作区背景", "sectionDescription": "在整个工作区背后显示一张图片。侧栏与面板会变半透明并磨砂让图片透出,遮罩保证文字可读。", diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json index 52f97aa0f..418a9ea11 100644 --- a/src/i18n/messages/zh-TW.json +++ b/src/i18n/messages/zh-TW.json @@ -173,6 +173,13 @@ "sectionDescription": "新會話頁面輸入框上方顯示的「程式開發 / 日常辦公」快捷卡片區域。", "showQuickActions": "在新會話頁面顯示" }, + "sidebarNav": { + "sectionTitle": "側邊欄導覽", + "sectionDescription": "選擇側邊欄在對話列表上方顯示哪些固定入口。隱藏的頁面仍可透過狀態列的「快捷操作」選單開啟。", + "showAutomations": "顯示自動化", + "showTasks": "顯示待辦任務", + "showForge": "顯示儲存庫面板" + }, "workspaceBackground": { "sectionTitle": "工作區背景", "sectionDescription": "在整個工作區背後顯示一張圖片。側邊欄與面板會變半透明並磨砂讓圖片透出,遮罩確保文字可讀。", diff --git a/src/lib/appearance-script.ts b/src/lib/appearance-script.ts index a0bcf4d21..52013bdaa 100644 --- a/src/lib/appearance-script.ts +++ b/src/lib/appearance-script.ts @@ -18,6 +18,13 @@ export const STORAGE_KEY_ZOOM_LEVEL = "codeg-zoom-level" // 缺省即回退为开启(保持历史行为);仅在欢迎态客户端渲染,无需预水合。 export const STORAGE_KEY_WELCOME_QUICK_ACTIONS = "codeg-welcome-quick-actions" +// Which of the sidebar's fixed nav rows (Automations / To-dos / Repository +// panel) are shown, stored as one JSON record. Absent or corrupt = every row +// visible (the historical behavior). Not pre-hydrated: the rows are part of +// the server-rendered markup, so the provider reconciles the stored value in +// a mount effect instead (same SSR/CSR rule as the sidebar's view toggles). +export const STORAGE_KEY_SIDEBAR_NAV_VISIBILITY = "codeg-sidebar-nav-visibility" + // 字体偏好(界面 / 编辑器 / 终端)。 // 只有界面字体需要 *_STACK(已解析的 CSS font-family 栈),供 inline 脚本零依赖地 // 预水合写入 --font-sans;编辑器/终端字体只走各自的 Monaco/xterm 选项,水合后才挂载, diff --git a/src/lib/sidebar-nav-visibility.test.ts b/src/lib/sidebar-nav-visibility.test.ts new file mode 100644 index 000000000..4bd943af3 --- /dev/null +++ b/src/lib/sidebar-nav-visibility.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from "vitest" + +import { + DEFAULT_SIDEBAR_NAV_VISIBILITY, + parseSidebarNavVisibility, +} from "./sidebar-nav-visibility" + +describe("parseSidebarNavVisibility", () => { + it("falls back to every row visible when nothing is stored", () => { + expect(parseSidebarNavVisibility(null)).toEqual({ + automations: true, + tasks: true, + forge: true, + }) + expect(parseSidebarNavVisibility("")).toEqual( + DEFAULT_SIDEBAR_NAV_VISIBILITY + ) + }) + + it("falls back on corrupt JSON and non-record shapes", () => { + expect(parseSidebarNavVisibility("not json")).toEqual( + DEFAULT_SIDEBAR_NAV_VISIBILITY + ) + expect(parseSidebarNavVisibility('["automations"]')).toEqual( + DEFAULT_SIDEBAR_NAV_VISIBILITY + ) + expect(parseSidebarNavVisibility("null")).toEqual( + DEFAULT_SIDEBAR_NAV_VISIBILITY + ) + }) + + it("round-trips a full record", () => { + const stored = JSON.stringify({ + automations: false, + tasks: true, + forge: false, + }) + expect(parseSidebarNavVisibility(stored)).toEqual({ + automations: false, + tasks: true, + forge: false, + }) + }) + + it("defaults rows missing from an older store to visible", () => { + // Forward compatibility: a row added in a later release must show up for + // users carrying a stored record that predates it, not vanish. + expect(parseSidebarNavVisibility('{"forge":false}')).toEqual({ + automations: true, + tasks: true, + forge: false, + }) + }) + + it("drops unknown keys and non-boolean values", () => { + const stored = JSON.stringify({ + automations: false, + forge: "false", + newChat: false, + }) + expect(parseSidebarNavVisibility(stored)).toEqual({ + automations: false, + tasks: true, + forge: true, + }) + }) +}) diff --git a/src/lib/sidebar-nav-visibility.ts b/src/lib/sidebar-nav-visibility.ts new file mode 100644 index 000000000..79e1efa72 --- /dev/null +++ b/src/lib/sidebar-nav-visibility.ts @@ -0,0 +1,51 @@ +/** The sidebar's fixed nav rows that can be hidden from Settings → + * Appearance. "New chat" is deliberately absent: it is the primary entry + * point into the workspace and must never disappear. Ids match the + * workbench route ids the rows navigate to. */ +export const HIDEABLE_SIDEBAR_NAV_IDS = [ + "automations", + "tasks", + "forge", +] as const + +export type HideableSidebarNavId = (typeof HIDEABLE_SIDEBAR_NAV_IDS)[number] + +/** Visibility of each hideable fixed nav row. Always a complete record — + * {@link parseSidebarNavVisibility} guarantees that on the way in, so + * consumers never need per-key fallbacks. */ +export type SidebarNavVisibility = Record + +export const DEFAULT_SIDEBAR_NAV_VISIBILITY: SidebarNavVisibility = { + automations: true, + tasks: true, + forge: true, +} + +/** + * Coerce the stored JSON into a complete visibility record. Absent or corrupt + * input falls back to every row visible (the historical layout); unknown keys + * are dropped and missing ones default to visible. That last step is what + * makes adding a NEW hideable row forward-compatible: an older stored record + * simply shows it instead of losing it. + */ +export function parseSidebarNavVisibility( + raw: string | null +): SidebarNavVisibility { + if (!raw) return DEFAULT_SIDEBAR_NAV_VISIBILITY + let parsed: unknown + try { + parsed = JSON.parse(raw) + } catch { + return DEFAULT_SIDEBAR_NAV_VISIBILITY + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return DEFAULT_SIDEBAR_NAV_VISIBILITY + } + const obj = parsed as Record + const out = { ...DEFAULT_SIDEBAR_NAV_VISIBILITY } + for (const id of HIDEABLE_SIDEBAR_NAV_IDS) { + const value = obj[id] + if (typeof value === "boolean") out[id] = value + } + return out +}
+ {t("sidebarNav.sectionDescription")} +