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),