(() => ({
observedKey: null,
@@ -495,7 +496,7 @@ export function AppSidebarLayout({
{children}
- {isElectron ? : null}
+ {isElectron && historySwipeNavigation ? : null}
);
diff --git a/apps/web/src/components/HistorySwipeNavigation.tsx b/apps/web/src/components/HistorySwipeNavigation.tsx
index 2924f82cd3136..d38d0a0bf211e 100644
--- a/apps/web/src/components/HistorySwipeNavigation.tsx
+++ b/apps/web/src/components/HistorySwipeNavigation.tsx
@@ -187,6 +187,7 @@ function SwipeIndicator({ direction, progress, y, bounds, tracking }: SwipeIndic
return (
diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx
index 633e18a567b2f..c928abc339504 100644
--- a/apps/web/src/components/settings/SettingsPanels.tsx
+++ b/apps/web/src/components/settings/SettingsPanels.tsx
@@ -587,6 +587,9 @@ export function useSettingsRestore(onRestored?: () => void) {
? ["Delete confirmation"]
: []),
...(settings.confirmQuit !== DEFAULT_UNIFIED_SETTINGS.confirmQuit ? ["Quit shortcut"] : []),
+ ...(settings.historySwipeNavigation !== DEFAULT_UNIFIED_SETTINGS.historySwipeNavigation
+ ? ["Swipe to navigate"]
+ : []),
...(isTextGenerationModelDirty ? ["Text generation model"] : []),
...(settings.threadTitleInstructions !== DEFAULT_UNIFIED_SETTINGS.threadTitleInstructions
? ["Thread title instructions"]
@@ -699,6 +702,7 @@ export function useSettingsRestore(onRestored?: () => void) {
confirmThreadDelete: DEFAULT_UNIFIED_SETTINGS.confirmThreadDelete,
confirmThreadUnpin: DEFAULT_UNIFIED_SETTINGS.confirmThreadUnpin,
confirmQuit: DEFAULT_UNIFIED_SETTINGS.confirmQuit,
+ historySwipeNavigation: DEFAULT_UNIFIED_SETTINGS.historySwipeNavigation,
textGenerationModelSelection: DEFAULT_UNIFIED_SETTINGS.textGenerationModelSelection,
threadTitleInstructions: DEFAULT_UNIFIED_SETTINGS.threadTitleInstructions,
fontFamilySans: DEFAULT_UNIFIED_SETTINGS.fontFamilySans,
@@ -932,6 +936,39 @@ export function GeneralSettingsPanel() {
/>
+ {isElectron && isMacPlatform(navigator.platform) ? (
+
+ Swipe horizontally with two fingers on a trackpad to go back or forward.
+ }
+ resetAction={
+ settings.historySwipeNavigation !==
+ DEFAULT_UNIFIED_SETTINGS.historySwipeNavigation ? (
+
+ updateSettings({
+ historySwipeNavigation: DEFAULT_UNIFIED_SETTINGS.historySwipeNavigation,
+ })
+ }
+ />
+ ) : null
+ }
+ control={
+
+ updateSettings({ historySwipeNavigation: Boolean(checked) })
+ }
+ aria-label={gt("Swipe to navigate")}
+ />
+ }
+ />
+
+ ) : null}
+
{
it("hides desktop-only settings from browser search", () => {
expect(SETTINGS_SEARCH_ITEMS.some((item) => item.id === "quit-confirmation")).toBe(true);
+ expect(SETTINGS_SEARCH_ITEMS.some((item) => item.id === "history-swipe-navigation")).toBe(true);
+ expect(searchSettings("horizontal wheel")).toEqual([]);
expect(searchSettings("hold to quit")).toEqual([]);
expect(searchSettings("wsl")).toEqual([]);
expect(searchSettings("update track")).toEqual([]);
diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts
index 7d7939e7f21f9..7713f88248720 100644
--- a/apps/web/src/components/settings/settingsSearch.ts
+++ b/apps/web/src/components/settings/settingsSearch.ts
@@ -853,6 +853,16 @@ export const SETTINGS_SEARCH_ITEMS = [
searchTerms: ["confirmation desktop app exit direct hold double click press twice"],
desktopOnly: true,
},
+ {
+ id: "history-swipe-navigation",
+ title: msg("Swipe to navigate"),
+ to: "/settings/general",
+ searchTerms: [
+ "two-finger horizontal trackpad gesture mouse wheel back forward thread history navigation",
+ ],
+ desktopOnly: true,
+ macOnly: true,
+ },
{
id: "text-generation-model",
title: "Text generation model",
diff --git a/docs/user/keybindings.md b/docs/user/keybindings.md
index 3e4d47e7c9433..36ab4cd2df981 100644
--- a/docs/user/keybindings.md
+++ b/docs/user/keybindings.md
@@ -184,6 +184,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 on macOS, a two-finger trackpad swipe does the same. Turn it off
+with **Settings → General → Navigation → 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 1db913e38ba3b..b6878ca353d09 100644
--- a/packages/contracts/src/settings.test.ts
+++ b/packages/contracts/src/settings.test.ts
@@ -338,6 +338,19 @@ describe("ClientSettings quit confirmation", () => {
});
});
+describe("ClientSettings history swipe navigation", () => {
+ it.each([undefined, true, false])("decodes and round-trips %s", (historySwipeNavigation) => {
+ const input = historySwipeNavigation === undefined ? {} : { historySwipeNavigation };
+ const settings = decodeClientSettings(input);
+
+ expect(settings.historySwipeNavigation).toBe(historySwipeNavigation ?? true);
+ expect(decodeClientSettings(encodeClientSettings(settings)).historySwipeNavigation).toBe(
+ historySwipeNavigation ?? true,
+ );
+ expect(decodeClientSettingsPatch(input)).toEqual(input);
+ });
+});
+
describe("provider enabled defaults", () => {
it("resolves instance enabled state with explicit false winning", () => {
const grok = ProviderDriverKind.make("grok");
diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts
index 7bbb03595d4bb..26f9f0c412e82 100644
--- a/packages/contracts/src/settings.ts
+++ b/packages/contracts/src/settings.ts
@@ -498,6 +498,7 @@ export const ClientSettingsSchema = Schema.Struct({
fontFamilyTerminal: FontFamilyPreference.pipe(Schema.withDecodingDefault(Effect.succeed(""))),
fontSmoothing: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
+ historySwipeNavigation: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
// Older installs also decode to null; onboarding must additionally require an empty workspace.
onboardingCompletedAt: Schema.NullOr(Schema.String).pipe(
Schema.withDecodingDefault(Effect.succeed(null)),
@@ -2072,6 +2073,7 @@ export const ClientSettingsPatch = Schema.Struct({
fontFamilySans: Schema.optionalKey(FontFamilyPreference),
fontFamilyTerminal: Schema.optionalKey(FontFamilyPreference),
fontSmoothing: Schema.optionalKey(Schema.Boolean),
+ historySwipeNavigation: Schema.optionalKey(Schema.Boolean),
persistComposerContextStrip: Schema.optionalKey(Schema.Boolean),
favorites: Schema.optionalKey(ModelFavorites),
providerModelPreferences: Schema.optionalKey(ProviderModelPreferences),