diff --git a/products/desktop/packages/ui/src/features/canvas/components/ChannelsSidebar.tsx b/products/desktop/packages/ui/src/features/canvas/components/ChannelsSidebar.tsx
index a2603abda259..06d333a662f0 100644
--- a/products/desktop/packages/ui/src/features/canvas/components/ChannelsSidebar.tsx
+++ b/products/desktop/packages/ui/src/features/canvas/components/ChannelsSidebar.tsx
@@ -29,6 +29,7 @@ import {
} from "@posthog/ui/features/canvas/stores/channelPaneStore";
import { useCurrentChannelStore } from "@posthog/ui/features/canvas/stores/currentChannelStore";
import { useOnboardingStore } from "@posthog/ui/features/onboarding/onboardingStore";
+import { useSettingsStore } from "@posthog/ui/features/settings/settingsStore";
import { NavResizeTooltip } from "@posthog/ui/features/sidebar/components/NavResizeTooltip";
import { ProjectSwitcher } from "@posthog/ui/features/sidebar/components/ProjectSwitcher";
import { SidebarMenu } from "@posthog/ui/features/sidebar/components/SidebarMenu";
@@ -158,8 +159,9 @@ function ChannelsSidebarImpl() {
const channelsLayout = useChannelsLayout();
const peek = useSidebarPeekStore((s) => s.peek);
+ const revealOnHover = useSettingsStore((s) => s.revealSidebarOnHover);
useSidebarEdgeHoverPeek({
- enabled: !open && !isResizing,
+ enabled: revealOnHover && !open && !isResizing,
peeked: peek,
side: "left",
width,
@@ -169,8 +171,8 @@ function ChannelsSidebarImpl() {
onClose: () => endSidebarPeek(),
});
useEffect(() => {
- if (open) cancelSidebarPeek();
- }, [open]);
+ if (open || !revealOnHover) cancelSidebarPeek();
+ }, [open, revealOnHover]);
// The peek store is a module-level singleton — if this sidebar unmounts
// while peeked (route without it), a stale peek would greet the remount.
useEffect(() => () => cancelSidebarPeek(), []);
diff --git a/products/desktop/packages/ui/src/features/settings/sections/GeneralSettings.tsx b/products/desktop/packages/ui/src/features/settings/sections/GeneralSettings.tsx
index 96f547e0b89a..7f43a152c61c 100644
--- a/products/desktop/packages/ui/src/features/settings/sections/GeneralSettings.tsx
+++ b/products/desktop/packages/ui/src/features/settings/sections/GeneralSettings.tsx
@@ -138,6 +138,7 @@ export function GeneralSettings() {
defaultCloudMessagingMode,
defaultReasoningEffort,
diffOpenMode,
+ revealSidebarOnHover,
sendMessagesWith,
setAutoConvertLongText,
setDefaultInitialTaskMode,
@@ -145,6 +146,7 @@ export function GeneralSettings() {
setDefaultCloudMessagingMode,
setDefaultReasoningEffort,
setDiffOpenMode,
+ setRevealSidebarOnHover,
setSendMessagesWith,
} = useSettingsStore();
@@ -160,6 +162,18 @@ export function GeneralSettings() {
[theme, setTheme],
);
+ const handleRevealSidebarOnHoverChange = useCallback(
+ (checked: boolean) => {
+ track(ANALYTICS_EVENTS.SETTING_CHANGED, {
+ setting_name: "reveal_sidebar_on_hover",
+ new_value: checked,
+ old_value: !checked,
+ });
+ setRevealSidebarOnHover(checked);
+ },
+ [setRevealSidebarOnHover],
+ );
+
const handleAutoConvertLongTextChange = useCallback(
(value: AutoConvertLongText) => {
track(ANALYTICS_EVENTS.SETTING_CHANGED, {
@@ -267,6 +281,18 @@ export function GeneralSettings() {
)}
+
+
+
+
+
{
["slotMachineMode", false, true],
["dismissibleUpdateBanners", false, true],
["showSidebarWorktrees", false, true],
+ ["revealSidebarOnHover", true, false],
] as const)("rehydrates %s", async (field, initial, persisted) => {
getItem.mockResolvedValue(
JSON.stringify({ state: { [field]: persisted }, version: 0 }),
diff --git a/products/desktop/packages/ui/src/features/settings/settingsStore.ts b/products/desktop/packages/ui/src/features/settings/settingsStore.ts
index 9bf05ee08226..d70710c71e6b 100644
--- a/products/desktop/packages/ui/src/features/settings/settingsStore.ts
+++ b/products/desktop/packages/ui/src/features/settings/settingsStore.ts
@@ -314,6 +314,11 @@ export interface SettingsStore {
// start a task in. Opt-in: off by default to keep the sidebar uncluttered.
showSidebarWorktrees: boolean;
setShowSidebarWorktrees: (enabled: boolean) => void;
+ // Slides the collapsed sidebar out when the pointer reaches the window edge
+ // or the title-bar toggle. On by default; off leaves the toggle and Cmd+B as
+ // the only ways to open it.
+ revealSidebarOnHover: boolean;
+ setRevealSidebarOnHover: (enabled: boolean) => void;
// Experimental / misc
hedgehogMode: boolean;
@@ -588,6 +593,9 @@ export const useSettingsStore = create()(
showSidebarWorktrees: false,
setShowSidebarWorktrees: (enabled) =>
set({ showSidebarWorktrees: enabled }),
+ revealSidebarOnHover: true,
+ setRevealSidebarOnHover: (enabled) =>
+ set({ revealSidebarOnHover: enabled }),
// Experimental / misc
hedgehogMode: false,
@@ -733,6 +741,7 @@ export const useSettingsStore = create()(
// Sidebar
showSidebarWorktrees: state.showSidebarWorktrees,
+ revealSidebarOnHover: state.revealSidebarOnHover,
// Experimental / misc
hedgehogMode: state.hedgehogMode,
diff --git a/products/desktop/packages/ui/src/primitives/hooks/useSidebarEdgeHoverPeek.test.ts b/products/desktop/packages/ui/src/primitives/hooks/useSidebarEdgeHoverPeek.test.ts
index 45e61f10f1b5..f1ed234b9717 100644
--- a/products/desktop/packages/ui/src/primitives/hooks/useSidebarEdgeHoverPeek.test.ts
+++ b/products/desktop/packages/ui/src/primitives/hooks/useSidebarEdgeHoverPeek.test.ts
@@ -3,8 +3,10 @@ import {
PEEK_REVEAL_THRESHOLD,
shouldCloseOnExit,
shouldRevealOnEdge,
+ useSidebarEdgeHoverPeek,
} from "@posthog/ui/primitives/hooks/useSidebarEdgeHoverPeek";
-import { describe, expect, it } from "vitest";
+import { renderHook } from "@testing-library/react";
+import { describe, expect, it, vi } from "vitest";
describe("shouldRevealOnEdge", () => {
const threshold = PEEK_REVEAL_THRESHOLD;
@@ -39,3 +41,31 @@ describe("shouldCloseOnExit", () => {
expect(shouldCloseOnExit({ pointer, width, margin })).toBe(expected);
});
});
+
+describe("useSidebarEdgeHoverPeek", () => {
+ const moveTo = (clientX: number): void => {
+ document.dispatchEvent(new MouseEvent("mousemove", { clientX }));
+ };
+
+ it.each([
+ ["reveals while enabled", true, 1],
+ ["stays closed while disabled", false, 0],
+ ])("%s", (_name, enabled, calls) => {
+ const onReveal = vi.fn();
+ renderHook(() =>
+ useSidebarEdgeHoverPeek({
+ enabled,
+ peeked: false,
+ side: "left",
+ width: 240,
+ onReveal,
+ onClose: vi.fn(),
+ }),
+ );
+
+ moveTo(400);
+ moveTo(2);
+
+ expect(onReveal).toHaveBeenCalledTimes(calls);
+ });
+});
diff --git a/products/desktop/packages/ui/src/router/routes/__root.tsx b/products/desktop/packages/ui/src/router/routes/__root.tsx
index 9027ea01a256..93fff1ef990c 100644
--- a/products/desktop/packages/ui/src/router/routes/__root.tsx
+++ b/products/desktop/packages/ui/src/router/routes/__root.tsx
@@ -46,6 +46,7 @@ import { useInboxDeepLink } from "@posthog/ui/features/inbox/hooks/useInboxDeepL
import { useIntegrations } from "@posthog/ui/features/integrations/useIntegrations";
import { useLoopDeepLink } from "@posthog/ui/features/loops/hooks/useLoopDeepLink";
import { useScoutDeepLink } from "@posthog/ui/features/scouts/hooks/useScoutDeepLink";
+import { useSettingsStore } from "@posthog/ui/features/settings/settingsStore";
import { useSetupDiscovery } from "@posthog/ui/features/setup/useSetupDiscovery";
import { NAV_RAIL_WIDTH } from "@posthog/ui/features/sidebar/constants";
import {
@@ -218,6 +219,7 @@ function RootLayout() {
const toggleSidebar = useSidebarStore((s) => s.toggle);
const sidebarPeek = useSidebarPeekStore((s) => s.peek);
+ const revealSidebarOnHover = useSettingsStore((s) => s.revealSidebarOnHover);
// Toggling makes any hover-peek redundant (opening replaces the overlay;
// closing must not leave it lingering under the pointer).
const handleToggleSidebar = (): void => {
@@ -348,7 +350,7 @@ function RootLayout() {
aria-label="Toggle sidebar"
onClick={handleToggleSidebar}
onMouseEnter={() => {
- if (!sidebarOpen) beginSidebarPeek();
+ if (revealSidebarOnHover && !sidebarOpen) beginSidebarPeek();
}}
>
{sidebarOpen ? (