Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/desktop/src/settings/DesktopClientSettings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const clientSettings: ClientSettings = {
fontSizeTerminal: 12,
fontSmoothing: true,
glassOpacity: 80,
historySwipeNavigation: false,
onboardingCompletedAt: null,
panelAnimationDurationMs: 0,
proactivePanelsEnabled: true,
Expand Down
5 changes: 3 additions & 2 deletions apps/web/src/components/AppSidebarLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -489,7 +490,7 @@ export function AppSidebarLayout({
{children}
<BackgroundThreadAnnouncer />
<SidebarControl showSidebar={showSidebar} />
{isElectron ? <HistorySwipeNavigation /> : null}
{isElectron && historySwipeNavigation ? <HistorySwipeNavigation /> : null}
</SidebarProvider>
</PanelAnimationSuppressionProvider>
);
Expand Down
32 changes: 32 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
: []),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1043,6 +1047,34 @@ export function GeneralSettingsPanel() {
</Select>
}
/>
{isElectron ? (
<SettingsRow
{...searchableSetting("history-swipe-navigation")}
description="Swipe two fingers sideways to go back or forward."
resetAction={
settings.historySwipeNavigation !==
DEFAULT_UNIFIED_SETTINGS.historySwipeNavigation ? (
<SettingResetButton
label="swipe to navigate"
onClick={() =>
updateSettings({
historySwipeNavigation: DEFAULT_UNIFIED_SETTINGS.historySwipeNavigation,
})
}
/>
) : null
}
control={
<Switch
checked={settings.historySwipeNavigation}
onCheckedChange={(checked) =>
updateSettings({ historySwipeNavigation: Boolean(checked) })
}
aria-label="Swipe to navigate"
/>
}
/>
) : null}
</SettingsSection>
</SettingsPageContainer>
);
Expand Down
7 changes: 7 additions & 0 deletions apps/web/src/components/settings/settingsSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 2 additions & 0 deletions docs/user/keybindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions packages/contracts/src/settings.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
2 changes: 2 additions & 0 deletions packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
),
Expand Down Expand Up @@ -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),
Expand Down
Loading