diff --git a/apps/desktop/src/settings/DesktopClientSettings.test.ts b/apps/desktop/src/settings/DesktopClientSettings.test.ts index 1ab9b3159e1f9..c0a612317e2a4 100644 --- a/apps/desktop/src/settings/DesktopClientSettings.test.ts +++ b/apps/desktop/src/settings/DesktopClientSettings.test.ts @@ -49,6 +49,7 @@ const clientSettings: ClientSettings = { fontSizeTerminal: 12, fontSmoothing: true, glassOpacity: 80, + historySwipeNavigation: false, onboardingCompletedAt: null, panelAnimationDurationMs: 0, proactivePanelsEnabled: true, diff --git a/apps/web/src/components/AppSidebarLayout.tsx b/apps/web/src/components/AppSidebarLayout.tsx index f9cb8c5170cf9..6cfe4b9bc3b5f 100644 --- a/apps/web/src/components/AppSidebarLayout.tsx +++ b/apps/web/src/components/AppSidebarLayout.tsx @@ -31,7 +31,7 @@ import { selectActiveRightPanel, useRightPanelStore } from "../rightPanelStore"; import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore"; import { cn, isMacPlatform } from "../lib/utils"; import { primaryServerKeybindingsAtom } from "../state/server"; -import { useEnvironmentIdentificationMode } from "../hooks/useSettings"; +import { useClientSettings, useEnvironmentIdentificationMode } from "../hooks/useSettings"; import { PanelAnimationSuppressionProvider, usePanelAnimationSettings, @@ -423,6 +423,7 @@ export function AppSidebarLayout({ const panelAnimationsSuppressed = usePanelNavigationSuppression(pathname); const routePanelAnimationsActive = panelAnimationsActive && !panelAnimationsSuppressed; const isMacosDesktop = isElectron && isMacPlatform(navigator.platform); + const historySwipeNavigation = useClientSettings((settings) => settings.historySwipeNavigation); const [sidebarWidth, setSidebarWidth] = useState(readInitialThreadSidebarWidth); // A clamped drag may not change width, so viewport changes need a separate subscription. const viewportWidth = useSyncExternalStore(subscribeToViewportWidth, readViewportWidth); @@ -489,7 +490,7 @@ export function AppSidebarLayout({ {children} - {isElectron ? : null} + {isElectron && historySwipeNavigation ? : null} ); diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index a37bf1581990e..0fe60beae3fae 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -476,6 +476,9 @@ export function useSettingsRestore(onRestored?: () => void) { ...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat ? ["Time format"] : []), + ...(settings.historySwipeNavigation !== DEFAULT_UNIFIED_SETTINGS.historySwipeNavigation + ? ["Swipe to navigate"] + : []), ...(settings.notificationMode !== DEFAULT_UNIFIED_SETTINGS.notificationMode ? ["Thread notifications"] : []), @@ -641,6 +644,7 @@ export function useSettingsRestore(onRestored?: () => void) { effortDisplay: DEFAULT_UNIFIED_SETTINGS.effortDisplay, mathRenderingEnabled: DEFAULT_UNIFIED_SETTINGS.mathRenderingEnabled, timestampFormat: DEFAULT_UNIFIED_SETTINGS.timestampFormat, + historySwipeNavigation: DEFAULT_UNIFIED_SETTINGS.historySwipeNavigation, notificationMode: DEFAULT_UNIFIED_SETTINGS.notificationMode, inAppNotificationsEnabled: DEFAULT_UNIFIED_SETTINGS.inAppNotificationsEnabled, wordWrap: DEFAULT_UNIFIED_SETTINGS.wordWrap, @@ -1043,6 +1047,34 @@ export function GeneralSettingsPanel() { } /> + {isElectron ? ( + + updateSettings({ + historySwipeNavigation: DEFAULT_UNIFIED_SETTINGS.historySwipeNavigation, + }) + } + /> + ) : null + } + control={ + + updateSettings({ historySwipeNavigation: Boolean(checked) }) + } + aria-label="Swipe to navigate" + /> + } + /> + ) : null} ); diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index f7492035874da..9c028a4ffa32e 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -676,6 +676,13 @@ export const SETTINGS_SEARCH_ITEMS = [ to: "/settings/general", searchTerms: ["timestamp clock locale system browser os 12 hour 24 hour"], }, + { + id: "history-swipe-navigation", + title: "Swipe to navigate", + to: "/settings/general", + searchTerms: ["trackpad two finger horizontal back forward history gesture desktop"], + desktopOnly: true, + }, { id: "response-streaming", title: "Response streaming", diff --git a/docs/user/keybindings.md b/docs/user/keybindings.md index 62079807fe438..c1124c2f599c4 100644 --- a/docs/user/keybindings.md +++ b/docs/user/keybindings.md @@ -174,6 +174,8 @@ field gets its own undo back. Terminals always keep their native shortcut. `navigation.back` (`mod+[` by default) and `navigation.forward` (`mod+]`) move through the pages you have visited, like a browser's back and forward buttons. +In the desktop app, a two-finger sideways swipe on the trackpad does the same. Turn it off in +Settings > General > Display > Swipe to navigate. `chat.new` may ask you to choose a project when there is more than one. `chat.newLocal` skips that chooser. Both use your diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index a5a27235bbe7c..fc6041f1df400 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -312,6 +312,21 @@ describe("ClientSettings notifications", () => { }); }); +describe("ClientSettings history swipe navigation", () => { + it("stays on for existing settings that omit it", () => { + expect(decodeClientSettings({}).historySwipeNavigation).toBe(true); + expect(decodeClientSettingsPatch({})).not.toHaveProperty("historySwipeNavigation"); + }); + + it("round-trips an explicit opt-out", () => { + expect( + decodeClientSettingsPatch({ historySwipeNavigation: false }).historySwipeNavigation, + ).toBe(false); + const settings = decodeClientSettings({ historySwipeNavigation: false }); + expect(encodeClientSettings(settings).historySwipeNavigation).toBe(false); + }); +}); + describe("ClientSettings quit confirmation", () => { it.each([ [true, "hold"], diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index cd112d84bf9f4..ed58c36509441 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -464,6 +464,7 @@ export const ClientSettingsSchema = Schema.Struct({ glassOpacity: GlassOpacity.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_GLASS_OPACITY)), ), + historySwipeNavigation: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), fontSizeInterface: InterfaceFontSize.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_INTERFACE_FONT_SIZE)), ), @@ -2004,6 +2005,7 @@ export const ClientSettingsPatch = Schema.Struct({ diffLayout: Schema.optionalKey(DiffLayout), environmentIdentificationMode: Schema.optionalKey(EnvironmentIdentificationMode), glassOpacity: Schema.optionalKey(GlassOpacity), + historySwipeNavigation: Schema.optionalKey(Schema.Boolean), onboardingCompletedAt: Schema.optionalKey(Schema.NullOr(Schema.String)), fontSizeInterface: Schema.optionalKey(InterfaceFontSize), fontSizePrompt: Schema.optionalKey(PromptFontSize),