diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx
index 95acb62765c98..2e3410f82c392 100644
--- a/apps/web/src/components/settings/SettingsPanels.tsx
+++ b/apps/web/src/components/settings/SettingsPanels.tsx
@@ -1,7 +1,7 @@
import { useAtomValue } from "@effect/atom-react";
import { NotificationSettings } from "./NotificationSettings";
import { PRIVACY_POLICY_URL } from "../../legalLinks";
-import { CheckIcon } from "lucide-react";
+import { CheckIcon, RotateCcwIcon } from "lucide-react";
import { msg, T, useGT, useMessages } from "gt-react";
import { Link, useNavigate } from "@tanstack/react-router";
import { useRef, useState } from "react";
@@ -59,7 +59,11 @@ import {
useUpdateScopedSettings,
} from "./useScopedSettings";
import { useScopedModelDisabledReason } from "./useScopedModelAvailability";
-import { useOptionalSettingsScope, useSettingsScope } from "./SettingsScopeContext";
+import {
+ SettingsScopeProvider,
+ useOptionalSettingsScope,
+ useSettingsScope,
+} from "./SettingsScopeContext";
import { scopedIdleCompaction } from "./scopedSettings";
import { ProjectDefaultsSettings } from "./ProjectDefaultsSettings";
import { ThreadTitleInstructionsSettings } from "./ThreadTitleInstructionsSettings";
@@ -427,7 +431,7 @@ function AboutVersionSection() {
);
}
-export function useSettingsRestore(onRestored?: () => void) {
+function useSettingsRestore(onRestored?: () => void) {
const {
theme,
setTheme,
@@ -717,6 +721,34 @@ export function useSettingsRestore(onRestored?: () => void) {
};
}
+export function RestoreDeviceDefaultsButton({ onRestored }: { onRestored: () => void }) {
+ const { search, selectScope } = useSettingsScope();
+ // Restore resets environment-wide settings, which a project or checkout scope cannot write.
+ return (
+
+
+
+ );
+}
+
+function RestoreDeviceDefaultsAction({ onRestored }: { onRestored: () => void }) {
+ const { changedSettingLabels, restoreDefaults } = useSettingsRestore(onRestored);
+ return (
+
+ );
+}
+
const AUTO_SETTLE_DEFAULT_DAYS = DEFAULT_UNIFIED_SETTINGS.sidebarAutoSettleAfterDays ?? 7;
function AutoSettleDaysInput({
diff --git a/apps/web/src/components/settings/SettingsRestore.browser.test.tsx b/apps/web/src/components/settings/SettingsRestore.browser.test.tsx
new file mode 100644
index 0000000000000..e723e3555f5bb
--- /dev/null
+++ b/apps/web/src/components/settings/SettingsRestore.browser.test.tsx
@@ -0,0 +1,141 @@
+import { RegistryContext } from "@effect/atom-react";
+import {
+ DEFAULT_SERVER_SETTINGS,
+ EnvironmentId,
+ ProjectId,
+ type ServerSettings,
+} from "@t3tools/contracts";
+import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings";
+import { DEFAULT_RESOLVED_KEYBINDINGS } from "@t3tools/shared/keybindings";
+import { AsyncResult } from "effect/reactivity";
+import { describe, expect, it, vi } from "vite-plus/test";
+import { userEvent } from "vite-plus/test/browser";
+
+import { render } from "../../../test/render";
+import type { Project } from "../../types";
+
+const state = vi.hoisted(() => ({
+ projects: [] as Project[],
+ environments: [] as unknown[],
+ updateSettings:
+ vi.fn<
+ (request: {
+ environmentId: unknown;
+ input: { patch: Record };
+ }) => Promise
+ >(),
+ confirm: vi.fn<() => Promise>(),
+}));
+
+vi.mock(import("~/state/environments"), async (original) => ({
+ ...(await original()),
+ useEnvironments: (() => ({ environments: state.environments })) as never,
+ usePrimaryEnvironmentId: () => primaryId,
+}));
+vi.mock(import("~/state/entities"), async (original) => ({
+ ...(await original()),
+ useProjects: (() => state.projects) as never,
+}));
+vi.mock(import("~/state/use-atom-command"), async (original) => ({
+ ...(await original()),
+ useAtomCommand: (() => state.updateSettings) as never,
+}));
+vi.mock(import("~/state/session"), async (original) => ({
+ ...(await original()),
+ readEnvironmentScope: () => true,
+}));
+vi.mock(import("~/localApi"), async (original) => ({
+ ...(await original()),
+ readLocalApi: (() => ({ dialogs: { confirm: state.confirm } })) as never,
+}));
+
+import { writeBrowserClientSettings } from "../../clientPersistenceStorage";
+import { appAtomRegistry } from "../../rpc/atomRegistry";
+import { buildSidebarProjectSnapshots } from "../../sidebarProjectGrouping";
+import { RestoreDeviceDefaultsButton } from "./SettingsPanels";
+import { SettingsScopeProvider } from "./SettingsScopeContext";
+
+const primaryId = EnvironmentId.make("primary");
+const remoteId = EnvironmentId.make("remote");
+const customized = {
+ ...DEFAULT_SERVER_SETTINGS,
+ sidebarAutoSettleAfterDays: 30,
+ enableProviderUpdateChecks: !DEFAULT_SERVER_SETTINGS.enableProviderUpdateChecks,
+};
+
+function setup(primarySettings: ServerSettings) {
+ vi.clearAllMocks();
+ // Browser test files share localStorage, and an earlier file may leave client settings changed.
+ writeBrowserClientSettings(DEFAULT_CLIENT_SETTINGS);
+ state.environments = (
+ [
+ [primaryId, primarySettings],
+ [remoteId, customized],
+ ] as const
+ ).map(([environmentId, settings]) => ({
+ environmentId,
+ label: environmentId,
+ connection: { phase: "connected" },
+ serverConfig: {
+ keybindings: DEFAULT_RESOLVED_KEYBINDINGS,
+ settings,
+ environment: { platform: { machine: "server" }, capabilities: { projectOrganization: true } },
+ },
+ }));
+ state.projects = [primaryId, remoteId].map((environmentId) => ({
+ id: ProjectId.make(`project-${environmentId}`),
+ environmentId,
+ title: "Shared repo",
+ workspaceRoot: `/work/${environmentId}`,
+ repositoryIdentity: {
+ canonicalKey: "github.com/example/shared-repo",
+ locator: {
+ source: "git-remote",
+ remoteName: "origin",
+ remoteUrl: "https://github.com/example/shared-repo.git",
+ },
+ },
+ defaultModelSelection: null,
+ createdAt: "2026-01-01T00:00:00.000Z",
+ updatedAt: "2026-01-01T00:00:00.000Z",
+ scripts: [],
+ }));
+ state.updateSettings.mockResolvedValue(AsyncResult.success(customized));
+ state.confirm.mockResolvedValue(true);
+ return buildSidebarProjectSnapshots({
+ projects: state.projects,
+ settings: DEFAULT_CLIENT_SETTINGS,
+ primaryEnvironmentId: primaryId,
+ resolveEnvironmentLabel: (id) => id,
+ })[0]!.projectKey;
+}
+
+describe("RestoreDeviceDefaultsButton", () => {
+ it("restores environment-wide defaults while a project is picked", async () => {
+ const projectKey = setup(customized);
+ const onRestored = vi.fn();
+ await using screen = await render(
+
+ {}}>
+
+
+ ,
+ );
+
+ const button = screen.getByRole("button", { name: "Restore device defaults" });
+ await expect.element(button).toBeEnabled();
+ await userEvent.click(button);
+ await vi.waitFor(() => expect(state.updateSettings).toHaveBeenCalledTimes(2));
+
+ expect(
+ state.updateSettings.mock.calls.map(([request]) => [
+ request.environmentId,
+ request.input.patch.sidebarAutoSettleAfterDays,
+ request.input.patch.projectSettingsOverrides,
+ ]),
+ ).toEqual([
+ [primaryId, DEFAULT_SERVER_SETTINGS.sidebarAutoSettleAfterDays, undefined],
+ [remoteId, DEFAULT_SERVER_SETTINGS.sidebarAutoSettleAfterDays, undefined],
+ ]);
+ });
+});
diff --git a/apps/web/src/routes/_chat.settings.tsx b/apps/web/src/routes/_chat.settings.tsx
index 7758c9ca55919..de4a7f86445f1 100644
--- a/apps/web/src/routes/_chat.settings.tsx
+++ b/apps/web/src/routes/_chat.settings.tsx
@@ -1,9 +1,7 @@
import { workspaceAreaProps } from "../lib/workspaceFocus";
import { Outlet, createFileRoute, redirect, useLocation } from "@tanstack/react-router";
import { useState, type ReactNode } from "react";
-import { RotateCcwIcon } from "lucide-react";
-import { Button } from "../components/ui/button";
-import { useSettingsRestore } from "../components/settings/SettingsPanels";
+import { RestoreDeviceDefaultsButton } from "../components/settings/SettingsPanels";
import { AppRouteModal, AppRouteModalClose } from "../components/AppRouteModal";
import { SettingsBreadcrumb } from "../components/settings/SettingsBreadcrumb";
@@ -27,21 +25,6 @@ import {
movedSettingsPath,
} from "../components/settings/settingsSearch";
-function RestoreDeviceDefaultsButton({ onRestored }: { onRestored: () => void }) {
- const { changedSettingLabels, restoreDefaults } = useSettingsRestore(onRestored);
- return (
-
- );
-}
-
function SettingsScopeBoundary({ pathname, children }: { pathname: string; children: ReactNode }) {
const { scope, connectedEnvironments } = useSettingsScope();
const { environments } = useEnvironments();