-
-
-
- {t.settings.account.email}
-
-
{user?.email ?? "—"}
-
- {t.settings.account.role}
-
-
- {user?.system_role ?? "—"}
-
+
+
+
+ {initial}
+
+
+
+ {user?.email ?? "—"}
+
+
+ {user?.system_role ?? "—"}
+
+
-
+
-
+
+
+
-
-
+
+
+
+
+ {t.settings.account.signOut}
+
+ }
+ />
+
);
diff --git a/frontend/src/components/workspace/settings/appearance-settings-page.tsx b/frontend/src/components/workspace/settings/appearance-settings-page.tsx
index 2e76d68e5a..28ea15b766 100644
--- a/frontend/src/components/workspace/settings/appearance-settings-page.tsx
+++ b/frontend/src/components/workspace/settings/appearance-settings-page.tsx
@@ -11,12 +11,11 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
-import { Separator } from "@/components/ui/separator";
import { enUS, isLocale, zhCN, type Locale } from "@/core/i18n";
import { useI18n } from "@/core/i18n/hooks";
import { cn } from "@/lib/utils";
-import { SettingsSection } from "./settings-section";
+import { SettingsCard, SettingsRow, SettingsSection } from "./settings-section";
const languageOptions: { value: Locale; label: string }[] = [
{ value: "en-US", label: enUS.locale.localName },
@@ -60,7 +59,7 @@ export function AppearanceSettingsPage() {
);
return (
-
+
-
-
-
-
}
- }}
- >
-
-
-
-
- {languageOptions.map((item) => (
-
- {item.label}
-
- ))}
-
-
+ />
+
);
diff --git a/frontend/src/components/workspace/settings/general-settings-page.tsx b/frontend/src/components/workspace/settings/general-settings-page.tsx
new file mode 100644
index 0000000000..442118a370
--- /dev/null
+++ b/frontend/src/components/workspace/settings/general-settings-page.tsx
@@ -0,0 +1,13 @@
+"use client";
+
+import { AppearanceSettingsPage } from "./appearance-settings-page";
+import { NotificationSettingsPage } from "./notification-settings-page";
+
+export function GeneralSettingsPage() {
+ return (
+
+ );
+}
diff --git a/frontend/src/components/workspace/settings/index.ts b/frontend/src/components/workspace/settings/index.ts
index 658450689b..87d3c38bcc 100644
--- a/frontend/src/components/workspace/settings/index.ts
+++ b/frontend/src/components/workspace/settings/index.ts
@@ -1 +1 @@
-export { SettingsDialog } from "./settings-dialog";
+export { SettingsShell } from "./settings-shell";
diff --git a/frontend/src/components/workspace/settings/memory-settings-page.tsx b/frontend/src/components/workspace/settings/memory-settings-page.tsx
index ce01d3c271..da917ba2a9 100644
--- a/frontend/src/components/workspace/settings/memory-settings-page.tsx
+++ b/frontend/src/components/workspace/settings/memory-settings-page.tsx
@@ -571,9 +571,16 @@ export function MemorySettingsPage() {
}}
variant="outline"
>
-
{filterAll}
-
{filterFacts}
-
+
+ {filterAll}
+
+
+ {filterFacts}
+
+
{filterSummaries}
@@ -591,6 +598,7 @@ export function MemorySettingsPage() {
variant="outline"
onClick={() => fileInputRef.current?.click()}
disabled={importMemoryMutation.isPending}
+ className="whitespace-nowrap"
>
{importButton}
@@ -599,11 +607,16 @@ export function MemorySettingsPage() {
variant="outline"
onClick={() => void handleExportMemory()}
disabled={isExporting}
+ className="whitespace-nowrap"
>
{isExporting ? t.common.loading : exportButton}
-
diff --git a/frontend/src/components/workspace/settings/notification-settings-page.tsx b/frontend/src/components/workspace/settings/notification-settings-page.tsx
index 5fbdb5002d..0e99fa19ec 100644
--- a/frontend/src/components/workspace/settings/notification-settings-page.tsx
+++ b/frontend/src/components/workspace/settings/notification-settings-page.tsx
@@ -8,7 +8,7 @@ import { useI18n } from "@/core/i18n/hooks";
import { useNotification } from "@/core/notification/hooks";
import { useLocalSettings } from "@/core/settings";
-import { SettingsSection } from "./settings-section";
+import { SettingsCard, SettingsRow, SettingsSection } from "./settings-section";
export function NotificationSettingsPage() {
const { t } = useI18n();
@@ -27,18 +27,13 @@ export function NotificationSettingsPage() {
});
};
- const handleEnableNotification = async (enabled: boolean) => {
- setSettings("notification", {
- enabled,
- });
+ const handleEnableNotification = (enabled: boolean) => {
+ setSettings("notification", { enabled });
};
if (!isSupported) {
return (
-
+
{t.settings.notification.notSupported}
@@ -47,12 +42,12 @@ export function NotificationSettingsPage() {
}
return (
-
- {t.settings.notification.description}
-
+
+
+
-
-
- }
- >
-
+ }
+ />
+
{permission === "default" && (
-
-
- {t.settings.notification.requestPermission}
-
+
+
+ {t.settings.notification.requestPermission}
+
+ }
+ />
)}
{permission === "denied" && (
-
- {t.settings.notification.deniedHint}
-
+
+
+ {t.settings.notification.deniedHint}
+
+
)}
{permission === "granted" && settings.notification.enabled && (
-
-
-
- {t.settings.notification.testButton}
-
-
+
+
+ {t.settings.notification.testButton}
+
+ }
+ />
)}
-
+
);
}
diff --git a/frontend/src/components/workspace/settings/settings-dialog.tsx b/frontend/src/components/workspace/settings/settings-dialog.tsx
deleted file mode 100644
index 6e9fa5ddf9..0000000000
--- a/frontend/src/components/workspace/settings/settings-dialog.tsx
+++ /dev/null
@@ -1,153 +0,0 @@
-"use client";
-
-import {
- BellIcon,
- InfoIcon,
- BrainIcon,
- PaletteIcon,
- SparklesIcon,
- UserIcon,
- WrenchIcon,
-} from "lucide-react";
-import { useEffect, useMemo, useState } from "react";
-
-import {
- Dialog,
- DialogContent,
- DialogHeader,
- DialogTitle,
-} from "@/components/ui/dialog";
-import { ScrollArea } from "@/components/ui/scroll-area";
-import { AboutSettingsPage } from "@/components/workspace/settings/about-settings-page";
-import { AccountSettingsPage } from "@/components/workspace/settings/account-settings-page";
-import { AppearanceSettingsPage } from "@/components/workspace/settings/appearance-settings-page";
-import { MemorySettingsPage } from "@/components/workspace/settings/memory-settings-page";
-import { NotificationSettingsPage } from "@/components/workspace/settings/notification-settings-page";
-import { SkillSettingsPage } from "@/components/workspace/settings/skill-settings-page";
-import { ToolSettingsPage } from "@/components/workspace/settings/tool-settings-page";
-import { useI18n } from "@/core/i18n/hooks";
-import { cn } from "@/lib/utils";
-
-type SettingsSection =
- | "account"
- | "appearance"
- | "memory"
- | "tools"
- | "skills"
- | "notification"
- | "about";
-
-type SettingsDialogProps = React.ComponentProps
& {
- defaultSection?: SettingsSection;
-};
-
-export function SettingsDialog(props: SettingsDialogProps) {
- const { defaultSection = "appearance", ...dialogProps } = props;
- const { t } = useI18n();
- const [activeSection, setActiveSection] =
- useState(defaultSection);
-
- useEffect(() => {
- // When opening the dialog, ensure the active section follows the caller's intent.
- // This allows triggers like "About" to open the dialog directly on that page.
- if (dialogProps.open) {
- setActiveSection(defaultSection);
- }
- }, [defaultSection, dialogProps.open]);
-
- const sections = useMemo(
- () => [
- {
- id: "account",
- label: t.settings.sections.account,
- icon: UserIcon,
- },
- {
- id: "appearance",
- label: t.settings.sections.appearance,
- icon: PaletteIcon,
- },
- {
- id: "notification",
- label: t.settings.sections.notification,
- icon: BellIcon,
- },
- {
- id: "memory",
- label: t.settings.sections.memory,
- icon: BrainIcon,
- },
- { id: "tools", label: t.settings.sections.tools, icon: WrenchIcon },
- { id: "skills", label: t.settings.sections.skills, icon: SparklesIcon },
- { id: "about", label: t.settings.sections.about, icon: InfoIcon },
- ],
- [
- t.settings.sections.account,
- t.settings.sections.appearance,
- t.settings.sections.memory,
- t.settings.sections.tools,
- t.settings.sections.skills,
- t.settings.sections.notification,
- t.settings.sections.about,
- ],
- );
- return (
-
- );
-}
diff --git a/frontend/src/components/workspace/settings/settings-section.tsx b/frontend/src/components/workspace/settings/settings-section.tsx
index 957ead8b72..751ca61e8a 100644
--- a/frontend/src/components/workspace/settings/settings-section.tsx
+++ b/frontend/src/components/workspace/settings/settings-section.tsx
@@ -1,5 +1,10 @@
import { cn } from "@/lib/utils";
+/**
+ * Top-level section under a settings page (e.g. "Theme", "Change password").
+ * Title sits flush with the page; description gives a one-line summary;
+ * children render below in a `mt-4` block.
+ */
export function SettingsSection({
className,
title,
@@ -7,19 +12,89 @@ export function SettingsSection({
children,
}: {
className?: string;
- title: React.ReactNode;
+ title?: React.ReactNode;
description?: React.ReactNode;
children: React.ReactNode;
}) {
+ const hasHeader = Boolean(title) || Boolean(description);
return (
-
- {title}
+ {hasHeader && (
+
+ {title && (
+ {title}
+ )}
+ {description && (
+ {description}
+ )}
+
+ )}
+ {children}
+
+ );
+}
+
+/**
+ * Bordered container that groups related rows. Children are separated by a
+ * subtle divider (`divide-y`).
+ */
+export function SettingsCard({
+ className,
+ children,
+}: {
+ className?: string;
+ children: React.ReactNode;
+}) {
+ return (
+
+ {children}
+
+ );
+}
+
+/**
+ * Single row within a SettingsCard. Label/description on the left, control on
+ * the right. Falls back to stacked layout on small screens.
+ *
+ * Use `size="compact"` for dense form rows (e.g. password fields) where the
+ * default vertical padding feels too airy.
+ */
+export function SettingsRow({
+ label,
+ description,
+ control,
+ className,
+ align = "center",
+ size = "default",
+}: {
+ label: React.ReactNode;
+ description?: React.ReactNode;
+ control: React.ReactNode;
+ className?: string;
+ align?: "center" | "start";
+ size?: "default" | "compact";
+}) {
+ return (
+
+
+
{label}
{description && (
-
{description}
+
{description}
)}
-
-
{children}
-
+
+
{control}
+
);
}
diff --git a/frontend/src/components/workspace/settings/settings-shell.tsx b/frontend/src/components/workspace/settings/settings-shell.tsx
new file mode 100644
index 0000000000..79ec9260d7
--- /dev/null
+++ b/frontend/src/components/workspace/settings/settings-shell.tsx
@@ -0,0 +1,119 @@
+"use client";
+
+import {
+ InfoIcon,
+ BrainIcon,
+ SettingsIcon,
+ SparklesIcon,
+ UserIcon,
+ WrenchIcon,
+} from "lucide-react";
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+import { useMemo } from "react";
+
+import { useI18n } from "@/core/i18n/hooks";
+import { cn } from "@/lib/utils";
+
+const SETTINGS_ROOT = "/workspace/settings";
+
+export function SettingsShell({ children }: { children: React.ReactNode }) {
+ const { t } = useI18n();
+ const pathname = usePathname();
+
+ const sections = useMemo(
+ () => [
+ {
+ id: "general",
+ href: `${SETTINGS_ROOT}/general`,
+ label: t.settings.sections.general,
+ icon: SettingsIcon,
+ },
+ {
+ id: "account",
+ href: `${SETTINGS_ROOT}/account`,
+ label: t.settings.sections.account,
+ icon: UserIcon,
+ },
+ {
+ id: "memory",
+ href: `${SETTINGS_ROOT}/memory`,
+ label: t.settings.sections.memory,
+ icon: BrainIcon,
+ },
+ {
+ id: "tools",
+ href: `${SETTINGS_ROOT}/tools`,
+ label: t.settings.sections.tools,
+ icon: WrenchIcon,
+ },
+ {
+ id: "skills",
+ href: `${SETTINGS_ROOT}/skills`,
+ label: t.settings.sections.skills,
+ icon: SparklesIcon,
+ },
+ {
+ id: "about",
+ href: `${SETTINGS_ROOT}/about`,
+ label: t.settings.sections.about,
+ icon: InfoIcon,
+ },
+ ],
+ [
+ t.settings.sections.general,
+ t.settings.sections.account,
+ t.settings.sections.memory,
+ t.settings.sections.tools,
+ t.settings.sections.skills,
+ t.settings.sections.about,
+ ],
+ );
+
+ return (
+
+
+
+ {/* Page header */}
+
+
+ {/* Two-column: sticky text nav + content */}
+
+
+
{children}
+
+
+
+
+ );
+}
diff --git a/frontend/src/components/workspace/settings/tool-settings-page.tsx b/frontend/src/components/workspace/settings/tool-settings-page.tsx
index dd3cd0d9d1..df5ad92c38 100644
--- a/frontend/src/components/workspace/settings/tool-settings-page.tsx
+++ b/frontend/src/components/workspace/settings/tool-settings-page.tsx
@@ -1,19 +1,12 @@
"use client";
-import {
- Item,
- ItemActions,
- ItemContent,
- ItemDescription,
- ItemTitle,
-} from "@/components/ui/item";
import { Switch } from "@/components/ui/switch";
import { useI18n } from "@/core/i18n/hooks";
import { useMCPConfig, useEnableMCPServer } from "@/core/mcp/hooks";
import type { MCPServerConfig } from "@/core/mcp/types";
import { env } from "@/env";
-import { SettingsSection } from "./settings-section";
+import { SettingsCard, SettingsRow, SettingsSection } from "./settings-section";
export function ToolSettingsPage() {
const { t } = useI18n();
@@ -26,7 +19,7 @@ export function ToolSettingsPage() {
{isLoading ? (
{t.common.loading}
) : error ? (
- Error: {error.message}
+ Error: {error.message}
) : (
config &&
)}
@@ -40,21 +33,18 @@ function MCPServerList({
servers: Record;
}) {
const { mutate: enableMCPServer } = useEnableMCPServer();
+ const entries = Object.entries(servers);
+ if (entries.length === 0) {
+ return null;
+ }
return (
-
- {Object.entries(servers).map(([name, config]) => (
-
-
-
-
-
-
-
- {config.description}
-
-
-
+
+ {entries.map(([name, config]) => (
+
-
-
+ }
+ />
))}
-
+
);
}
diff --git a/frontend/src/components/workspace/workspace-nav-menu.tsx b/frontend/src/components/workspace/workspace-nav-menu.tsx
index 8b99be0789..7b1022b905 100644
--- a/frontend/src/components/workspace/workspace-nav-menu.tsx
+++ b/frontend/src/components/workspace/workspace-nav-menu.tsx
@@ -9,6 +9,7 @@ import {
Settings2Icon,
SettingsIcon,
} from "lucide-react";
+import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import {
@@ -28,7 +29,6 @@ import {
import { useI18n } from "@/core/i18n/hooks";
import { GithubIcon } from "./github-icon";
-import { SettingsDialog } from "./settings";
function NavMenuButtonContent({
isSidebarOpen,
@@ -51,10 +51,7 @@ function NavMenuButtonContent({
}
export function WorkspaceNavMenu() {
- const [settingsOpen, setSettingsOpen] = useState(false);
- const [settingsDefaultSection, setSettingsDefaultSection] = useState<
- "appearance" | "memory" | "tools" | "skills" | "notification" | "about"
- >("appearance");
+ const router = useRouter();
const [mounted, setMounted] = useState(false);
const { open: isSidebarOpen } = useSidebar();
const { t } = useI18n();
@@ -65,11 +62,6 @@ export function WorkspaceNavMenu() {
return (
<>
-
{mounted ? (
@@ -90,8 +82,7 @@ export function WorkspaceNavMenu() {
{
- setSettingsDefaultSection("appearance");
- setSettingsOpen(true);
+ router.push("/workspace/settings/general");
}}
>
@@ -139,8 +130,7 @@ export function WorkspaceNavMenu() {
{
- setSettingsDefaultSection("about");
- setSettingsOpen(true);
+ router.push("/workspace/settings/about");
}}
>
diff --git a/frontend/src/core/i18n/locales/en-US.ts b/frontend/src/core/i18n/locales/en-US.ts
index d2b9538b94..10b11f847f 100644
--- a/frontend/src/core/i18n/locales/en-US.ts
+++ b/frontend/src/core/i18n/locales/en-US.ts
@@ -351,6 +351,7 @@ export const enUS: Translations = {
title: "Settings",
description: "Adjust how DeerFlow looks and behaves for you.",
sections: {
+ general: "General",
account: "Account",
appearance: "Appearance",
memory: "Memory",
@@ -365,9 +366,9 @@ export const enUS: Translations = {
"DeerFlow automatically learns from your conversations in the background. These memories help DeerFlow understand you better and deliver a more personalized experience.",
empty: "No memory data to display.",
rawJson: "Raw JSON",
- exportButton: "Export memory",
+ exportButton: "Export",
exportSuccess: "Memory exported",
- importButton: "Import memory",
+ importButton: "Import",
importConfirmTitle: "Import memory?",
importConfirmDescription:
"This will overwrite your current memory with the selected JSON backup.",
@@ -381,7 +382,7 @@ export const enUS: Translations = {
editFactTitle: "Edit memory fact",
addFactSuccess: "Fact created",
editFactSuccess: "Fact updated",
- clearAll: "Clear all memory",
+ clearAll: "Clear all",
clearAllConfirmTitle: "Clear all memory?",
clearAllConfirmDescription:
"This will remove all saved summaries and facts. This action cannot be undone.",
@@ -493,7 +494,10 @@ export const enUS: Translations = {
networkError: "Network error. Please try again.",
updating: "Updating...",
updatePassword: "Update Password",
+ sessionTitle: "Session",
signOut: "Sign Out",
+ signOutDescription:
+ "Sign out of this device. You can sign back in any time.",
},
acknowledge: {
emptyTitle: "Acknowledgements",
diff --git a/frontend/src/core/i18n/locales/types.ts b/frontend/src/core/i18n/locales/types.ts
index 79e279192f..8cf001a68e 100644
--- a/frontend/src/core/i18n/locales/types.ts
+++ b/frontend/src/core/i18n/locales/types.ts
@@ -278,6 +278,7 @@ export interface Translations {
title: string;
description: string;
sections: {
+ general: string;
account: string;
appearance: string;
memory: string;
@@ -409,7 +410,9 @@ export interface Translations {
networkError: string;
updating: string;
updatePassword: string;
+ sessionTitle: string;
signOut: string;
+ signOutDescription: string;
};
acknowledge: {
emptyTitle: string;
diff --git a/frontend/src/core/i18n/locales/zh-CN.ts b/frontend/src/core/i18n/locales/zh-CN.ts
index c4fc6b9457..7d968cf501 100644
--- a/frontend/src/core/i18n/locales/zh-CN.ts
+++ b/frontend/src/core/i18n/locales/zh-CN.ts
@@ -335,6 +335,7 @@ export const zhCN: Translations = {
title: "设置",
description: "根据你的偏好调整 DeerFlow 的界面和行为。",
sections: {
+ general: "通用",
account: "账号",
appearance: "外观",
memory: "记忆",
@@ -473,7 +474,9 @@ export const zhCN: Translations = {
networkError: "网络错误,请重试。",
updating: "更新中...",
updatePassword: "修改密码",
+ sessionTitle: "会话",
signOut: "退出登录",
+ signOutDescription: "退出当前设备的登录,你可以随时重新登录。",
},
acknowledge: {
emptyTitle: "致谢",