From 1ebad38660ed9d5ad9ce461151e15392ec2c1e9c Mon Sep 17 00:00:00 2001 From: anduimagui Date: Wed, 11 Mar 2026 16:02:27 +0000 Subject: [PATCH] fix(web): close settings with Escape --- apps/web/src/lib/settingsNavigation.test.ts | 35 +++++++++++++++++++++ apps/web/src/lib/settingsNavigation.ts | 28 +++++++++++++++++ apps/web/src/routes/_chat.settings.tsx | 29 +++++++++++++++-- 3 files changed, 90 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/lib/settingsNavigation.test.ts create mode 100644 apps/web/src/lib/settingsNavigation.ts diff --git a/apps/web/src/lib/settingsNavigation.test.ts b/apps/web/src/lib/settingsNavigation.test.ts new file mode 100644 index 0000000000..007d31bb92 --- /dev/null +++ b/apps/web/src/lib/settingsNavigation.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; + +import { canNavigateBackInApp, shouldCloseSettingsOnEscape } from "./settingsNavigation"; + +describe("shouldCloseSettingsOnEscape", () => { + it("matches plain Escape", () => { + expect(shouldCloseSettingsOnEscape({ key: "Escape" })).toBe(true); + }); + + it("ignores modified Escape shortcuts", () => { + expect(shouldCloseSettingsOnEscape({ key: "Escape", metaKey: true })).toBe(false); + expect(shouldCloseSettingsOnEscape({ key: "Escape", ctrlKey: true })).toBe(false); + expect(shouldCloseSettingsOnEscape({ key: "Escape", altKey: true })).toBe(false); + expect(shouldCloseSettingsOnEscape({ key: "Escape", shiftKey: true })).toBe(false); + }); + + it("ignores prevented and non-Escape events", () => { + expect(shouldCloseSettingsOnEscape({ key: "Escape", defaultPrevented: true })).toBe(false); + expect(shouldCloseSettingsOnEscape({ key: "Enter" })).toBe(false); + }); +}); + +describe("canNavigateBackInApp", () => { + it("returns true when tanstack router history has a previous entry", () => { + expect(canNavigateBackInApp({ __TSR_index: 1 })).toBe(true); + expect(canNavigateBackInApp({ __TSR_index: 4 })).toBe(true); + }); + + it("returns false for empty or external history state", () => { + expect(canNavigateBackInApp(null)).toBe(false); + expect(canNavigateBackInApp({})).toBe(false); + expect(canNavigateBackInApp({ __TSR_index: 0 })).toBe(false); + expect(canNavigateBackInApp({ __TSR_index: "1" })).toBe(false); + }); +}); diff --git a/apps/web/src/lib/settingsNavigation.ts b/apps/web/src/lib/settingsNavigation.ts new file mode 100644 index 0000000000..1f1a449e77 --- /dev/null +++ b/apps/web/src/lib/settingsNavigation.ts @@ -0,0 +1,28 @@ +export interface SettingsEscapeEventLike { + key: string; + defaultPrevented?: boolean; + metaKey?: boolean; + ctrlKey?: boolean; + altKey?: boolean; + shiftKey?: boolean; +} + +export function shouldCloseSettingsOnEscape(event: SettingsEscapeEventLike): boolean { + return ( + event.key === "Escape" && + event.defaultPrevented !== true && + event.metaKey !== true && + event.ctrlKey !== true && + event.altKey !== true && + event.shiftKey !== true + ); +} + +export function canNavigateBackInApp(historyState: unknown): boolean { + if (!historyState || typeof historyState !== "object") { + return false; + } + + const index = (historyState as { __TSR_index?: unknown }).__TSR_index; + return typeof index === "number" && index > 0; +} diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx index 93e0744421..505f1b0c05 100644 --- a/apps/web/src/routes/_chat.settings.tsx +++ b/apps/web/src/routes/_chat.settings.tsx @@ -1,12 +1,13 @@ -import { createFileRoute } from "@tanstack/react-router"; +import { createFileRoute, useNavigate } from "@tanstack/react-router"; import { useQuery } from "@tanstack/react-query"; -import { useCallback, useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { type ProviderKind } from "@t3tools/contracts"; import { getModelOptions, normalizeModelSlug } from "@t3tools/shared/model"; import { MAX_CUSTOM_MODEL_LENGTH, useAppSettings } from "../appSettings"; import { isElectron } from "../env"; import { useTheme } from "../hooks/useTheme"; +import { canNavigateBackInApp, shouldCloseSettingsOnEscape } from "../lib/settingsNavigation"; import { serverConfigQueryOptions } from "../lib/serverReactQuery"; import { ensureNativeApi } from "../nativeApi"; import { preferredTerminalEditor } from "../terminal-links"; @@ -81,6 +82,7 @@ function patchCustomModels(provider: ProviderKind, models: string[]) { } function SettingsRouteView() { + const navigate = useNavigate(); const { theme, setTheme, resolvedTheme } = useTheme(); const { settings, defaults, updateSettings } = useAppSettings(); const serverConfigQuery = useQuery(serverConfigQueryOptions()); @@ -99,6 +101,29 @@ function SettingsRouteView() { const codexHomePath = settings.codexHomePath; const keybindingsConfigPath = serverConfigQuery.data?.keybindingsConfigPath ?? null; + useEffect(() => { + const onWindowKeyDown = (event: KeyboardEvent) => { + if (!shouldCloseSettingsOnEscape(event)) { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + if (canNavigateBackInApp(window.history.state)) { + window.history.back(); + return; + } + + void navigate({ to: "/", replace: true }); + }; + + window.addEventListener("keydown", onWindowKeyDown); + return () => { + window.removeEventListener("keydown", onWindowKeyDown); + }; + }, [navigate]); + const openKeybindingsFile = useCallback(() => { if (!keybindingsConfigPath) return; setOpenKeybindingsError(null);