+ {/* Desktop Settings modal — mounted over whatever page is rendered
+ beneath when the URL is a settings path. */}
+ {settingsOpen && !chromeless && }
{/* Hidden Remotion-driven producer for the Meet camera. Mounts a
640×480 JPEG frame stream to the Rust frame bus while a meet
diff --git a/app/src/AppRoutes.tsx b/app/src/AppRoutes.tsx
index 49648e851..db11a97c8 100644
--- a/app/src/AppRoutes.tsx
+++ b/app/src/AppRoutes.tsx
@@ -1,4 +1,4 @@
-import { Navigate, Route, Routes } from 'react-router-dom';
+import { type Location, Navigate, Route, Routes } from 'react-router-dom';
import AgentWorldShell from './agentworld/AgentWorldShell';
import AgentWorld from './agentworld/pages/AgentWorld';
@@ -17,14 +17,23 @@ import Notifications from './pages/Notifications';
import Onboarding from './pages/onboarding/Onboarding';
import { PttOverlayPage } from './pages/PttOverlayPage';
import Rewards from './pages/Rewards';
-import Settings from './pages/Settings';
import Skills from './pages/Skills';
import WebCallbackPage from './pages/WebCallbackPage';
import Welcome from './pages/Welcome';
import WorkflowNew from './pages/WorkflowNew';
import WorkflowsRun from './pages/WorkflowsRun';
-const AppRoutes = () => {
+interface AppRoutesProps {
+ /**
+ * Optional location override. The desktop shell passes the *background*
+ * location here while the Settings modal is open, so the page behind the
+ * modal stays rendered even though the URL is `/settings/*`. Omitted
+ * everywhere else (router uses the ambient location).
+ */
+ location?: Location | string;
+}
+
+const AppRoutes = ({ location }: AppRoutesProps = {}) => {
// Mobile target (iOS or Android): pair → Human/Chat/Settings only.
// Desktop routes are not rendered.
if (getIsMobile()) {
@@ -32,7 +41,7 @@ const AppRoutes = () => {
}
return (
-
+
{/* Public routes - redirect to /home if logged in */}
{
} />
-
-
-
- }
- />
+ {/* Desktop Settings renders as a modal overlay mounted by AppShellDesktop
+ (App.tsx) using the backgroundLocation pattern — it is no longer an
+ inline route here. iOS keeps its own /settings/* route in
+ AppRoutesIOS.tsx. */}
} />
diff --git a/app/src/components/intelligence/IntelligenceSubconsciousTab.tsx b/app/src/components/intelligence/IntelligenceSubconsciousTab.tsx
index f3404f553..1248408a7 100644
--- a/app/src/components/intelligence/IntelligenceSubconsciousTab.tsx
+++ b/app/src/components/intelligence/IntelligenceSubconsciousTab.tsx
@@ -1,9 +1,10 @@
import { useCallback, useEffect, useState } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router-dom';
import { useT } from '../../lib/i18n/I18nContext';
import type { SubconsciousMode } from '../../utils/tauriCommands/heartbeat';
import type { SubconsciousStatus } from '../../utils/tauriCommands/subconscious';
+import { settingsNavState } from '../settings/modal/settingsOverlay';
interface ModeOption {
id: SubconsciousMode;
@@ -67,6 +68,7 @@ export default function IntelligenceSubconsciousTab({
}: IntelligenceSubconsciousTabProps) {
const { t } = useT();
const navigate = useNavigate();
+ const location = useLocation();
const providerUnavailable = status?.provider_available === false;
const providerUnavailableReason = providerUnavailable
? (status?.provider_unavailable_reason ?? t('subconscious.providerUnavailableTitle'))
@@ -238,7 +240,7 @@ export default function IntelligenceSubconsciousTab({
diff --git a/app/src/components/intelligence/IntelligenceTasksTab.tsx b/app/src/components/intelligence/IntelligenceTasksTab.tsx
index 5321e259f..af730c26f 100644
--- a/app/src/components/intelligence/IntelligenceTasksTab.tsx
+++ b/app/src/components/intelligence/IntelligenceTasksTab.tsx
@@ -24,7 +24,7 @@ import {
LuSparkles,
LuX,
} from 'react-icons/lu';
-import { useNavigate } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router-dom';
import { useT } from '../../lib/i18n/I18nContext';
import { TaskKanbanBoard } from '../../pages/conversations/components/TaskKanbanBoard';
@@ -48,6 +48,7 @@ import {
import type { ThreadMessage } from '../../types/thread';
import type { TaskBoard, TaskBoardCard, TaskBoardCardStatus } from '../../types/turnState';
import { chatThreadPath } from '../../utils/chatRoutes';
+import { settingsNavState } from '../settings/modal/settingsOverlay';
import { UserTaskComposer } from './UserTaskComposer';
const log = debug('intelligence:tasks');
@@ -779,6 +780,7 @@ function TaskSourceTaskList({
}) {
const { t } = useT();
const navigate = useNavigate();
+ const location = useLocation();
const sortedCards = useMemo(
() => [...board.cards].sort((a, b) => a.order - b.order),
[board.cards]
@@ -797,7 +799,7 @@ function TaskSourceTaskList({
diff --git a/app/src/components/intelligence/__tests__/IntelligenceSubconsciousTab.test.tsx b/app/src/components/intelligence/__tests__/IntelligenceSubconsciousTab.test.tsx
index b53b672a7..c51dbaad4 100644
--- a/app/src/components/intelligence/__tests__/IntelligenceSubconsciousTab.test.tsx
+++ b/app/src/components/intelligence/__tests__/IntelligenceSubconsciousTab.test.tsx
@@ -9,7 +9,16 @@ import IntelligenceSubconsciousTab from '../IntelligenceSubconsciousTab';
const mockNavigate = vi.fn();
-vi.mock('react-router-dom', () => ({ useNavigate: () => mockNavigate }));
+vi.mock('react-router-dom', () => ({
+ useNavigate: () => mockNavigate,
+ useLocation: () => ({
+ pathname: '/intelligence',
+ search: '',
+ hash: '',
+ state: null,
+ key: 'test',
+ }),
+}));
function baseProps(): ComponentProps {
return {
diff --git a/app/src/components/intelligence/__tests__/IntelligenceTasksTab.test.tsx b/app/src/components/intelligence/__tests__/IntelligenceTasksTab.test.tsx
index 9a34d32c8..a0f6347f1 100644
--- a/app/src/components/intelligence/__tests__/IntelligenceTasksTab.test.tsx
+++ b/app/src/components/intelligence/__tests__/IntelligenceTasksTab.test.tsx
@@ -67,7 +67,17 @@ vi.mock('../../../store/hooks', () => ({
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual('react-router-dom');
- return { ...actual, useNavigate: () => hoisted.navigate };
+ return {
+ ...actual,
+ useNavigate: () => hoisted.navigate,
+ useLocation: () => ({
+ pathname: '/intelligence',
+ search: '',
+ hash: '',
+ state: null,
+ key: 'test',
+ }),
+ };
});
// Stub the composer so we can drive its `onCreated` callback without
@@ -276,7 +286,7 @@ describe('IntelligenceTasksTab', () => {
// "Manage sources" jumps to the merged Integrations settings page
// (task-sources was folded into /settings/integrations).
fireEvent.click(screen.getByText('Manage sources'));
- expect(hoisted.navigate).toHaveBeenCalledWith('/settings/integrations');
+ expect(hoisted.navigate).toHaveBeenCalledWith('/settings/integrations', expect.anything());
});
test('refines a source task and approves it into the personal agent board', async () => {
diff --git a/app/src/components/layout/shell/CollapsedNavRail.test.tsx b/app/src/components/layout/shell/CollapsedNavRail.test.tsx
index cfea2e01e..9b0508992 100644
--- a/app/src/components/layout/shell/CollapsedNavRail.test.tsx
+++ b/app/src/components/layout/shell/CollapsedNavRail.test.tsx
@@ -37,7 +37,11 @@ describe('CollapsedNavRail', () => {
it('wallet button navigates to /settings/wallet-balances', () => {
renderWithProviders(, { initialEntries: ['/home'] });
fireEvent.click(screen.getByRole('button', { name: 'nav.wallet' }));
- expect(mockNavigate).toHaveBeenCalledWith('/settings/wallet-balances');
+ // Carries the backgroundLocation so the desktop Settings modal renders over
+ // the page it was opened from.
+ expect(mockNavigate).toHaveBeenCalledWith('/settings/wallet-balances', {
+ state: { backgroundLocation: expect.objectContaining({ pathname: '/home' }) },
+ });
});
it('wallet button has correct data-analytics-id', () => {
diff --git a/app/src/components/layout/shell/CollapsedNavRail.tsx b/app/src/components/layout/shell/CollapsedNavRail.tsx
index a2019f61f..2282d0439 100644
--- a/app/src/components/layout/shell/CollapsedNavRail.tsx
+++ b/app/src/components/layout/shell/CollapsedNavRail.tsx
@@ -6,6 +6,7 @@ import { useT } from '../../../lib/i18n/I18nContext';
import { trackEvent } from '../../../services/analytics';
import { useAppSelector } from '../../../store/hooks';
import { selectUnreadCount } from '../../../store/notificationSlice';
+import { settingsNavState } from '../../settings/modal/settingsOverlay';
import { Tooltip } from '../../ui';
import { NavIcon } from './navIcons';
import { useHomeNav } from './useHomeNav';
@@ -78,7 +79,7 @@ export default function CollapsedNavRail() {
{!profile.builtIn && (
diff --git a/app/src/components/settings/settingsRouteElements.tsx b/app/src/components/settings/settingsRouteElements.tsx
new file mode 100644
index 000000000..d489b67c9
--- /dev/null
+++ b/app/src/components/settings/settingsRouteElements.tsx
@@ -0,0 +1,226 @@
+import type { ReactNode } from 'react';
+import { Navigate, Route, useLocation } from 'react-router-dom';
+
+import WorkflowsTab from '../intelligence/WorkflowsTab';
+import SettingsIndexRedirect from './layout/SettingsIndexRedirect';
+import AboutPanel from './panels/AboutPanel';
+import AccountPanel from './panels/AccountPanel';
+import AgentAccessPanel from './panels/AgentAccessPanel';
+import AgentActivityPanel from './panels/AgentActivityPanel';
+import AgentBoxPanel from './panels/AgentBoxPanel';
+import AgentChatPanel from './panels/AgentChatPanel';
+import AgentEditorPage from './panels/AgentEditorPage';
+import AgentsPanel from './panels/AgentsPanel';
+import AppearancePanel from './panels/AppearancePanel';
+import ApprovalHistoryPanel from './panels/ApprovalHistoryPanel';
+import AutocompleteDebugPanel from './panels/AutocompleteDebugPanel';
+import AutocompletePanel from './panels/AutocompletePanel';
+import BillingPanel from './panels/BillingPanel';
+import CompanionPanel from './panels/CompanionPanel';
+import ComposioTriagePanel from './panels/ComposioTriagePanel';
+import CronJobsPanel from './panels/CronJobsPanel';
+import DesktopAgentPanel from './panels/DesktopAgentPanel';
+import DeveloperOptionsPanel from './panels/DeveloperOptionsPanel';
+import DevicesPanel from './panels/DevicesPanel';
+import DevWorkflowPanel from './panels/DevWorkflowPanel';
+import EventLogPanel from './panels/EventLogPanel';
+import IntegrationsPanel from './panels/IntegrationsPanel';
+import LocalModelDebugPanel from './panels/LocalModelDebugPanel';
+import McpServerPanel from './panels/McpServerPanel';
+import MeetingSettingsPanel from './panels/MeetingSettingsPanel';
+import MemorySyncPanel from './panels/MemorySyncPanel';
+import MigrationPanel from './panels/MigrationPanel';
+import ModelHealthPanel from './panels/ModelHealthPanel';
+import NotificationsTabbedPanel from './panels/NotificationsTabbedPanel';
+import PermissionsPanel from './panels/PermissionsPanel';
+import PersonalityPanel from './panels/PersonalityPanel';
+import PrivacyPanel from './panels/PrivacyPanel';
+import ProfileEditorPage from './panels/ProfileEditorPage';
+import ProfilesPanel from './panels/ProfilesPanel';
+import RecoveryPhrasePanel from './panels/RecoveryPhrasePanel';
+import SandboxSettingsPanel from './panels/SandboxSettingsPanel';
+import ScreenAwarenessDebugPanel from './panels/ScreenAwarenessDebugPanel';
+import ScreenIntelligencePanel from './panels/ScreenIntelligencePanel';
+import SecurityPanel from './panels/SecurityPanel';
+import TasksPanel from './panels/TasksPanel';
+import TeamInvitesPanel from './panels/TeamInvitesPanel';
+import TeamManagementPanel from './panels/TeamManagementPanel';
+import TeamMembersPanel from './panels/TeamMembersPanel';
+import TeamPanel from './panels/TeamPanel';
+import ToolPolicyDiagnosticsPanel from './panels/ToolPolicyDiagnosticsPanel';
+import ToolsPanel from './panels/ToolsPanel';
+import UsagePanel from './panels/UsagePanel';
+import VoiceDebugPanel from './panels/VoiceDebugPanel';
+import WalletBalancesPanel from './panels/WalletBalancesPanel';
+import WebhooksDebugPanel from './panels/WebhooksDebugPanel';
+import WorkflowRunnerPanel from './panels/WorkflowRunnerPanel';
+
+/**
+ * Single vertical-scroll wrapper for a settings panel. The surrounding card
+ * (bg / border / rounding) is provided by the host — `SettingsLayout`'s content
+ * pane on iOS, or `SettingsModalLayout`'s right column on desktop — so panels
+ * sit directly on it. PanelScaffold-based panels are `h-full` and own their own
+ * internal scroll; legacy panels that overflow scroll here. Either way there's
+ * exactly one scrollbar.
+ */
+export const WrappedSettingsPage = ({ children }: { children: ReactNode }) => {
+ return
{children}
;
+};
+
+const wrapSettingsPage = (element: ReactNode) => (
+ {element}
+);
+
+/**
+ * Redirect that stays *within* `/settings/*` while preserving nav state — most
+ * importantly the desktop modal's `backgroundLocation`, so a legacy-slug hop
+ * inside the modal keeps its backdrop instead of falling back to the default
+ * page. Use this for in-settings redirects only; external redirects (`/brain`,
+ * `/connections`) intentionally exit the modal and keep a plain ``.
+ */
+const SettingsRedirect = ({ to }: { to: string }) => {
+ const location = useLocation();
+ return ;
+};
+
+/**
+ * The full settings route table — index, every panel, and every legacy-slug
+ * redirect. Returned as a fragment of `` elements (via a function call,
+ * not a nested component) so it can be embedded directly inside a `` in
+ * both hosts:
+ *
+ * - Desktop modal: `{settingsRouteElements()}`
+ * - iOS full page: `}>{settingsRouteElements()}`
+ *
+ * Retired slugs are kept as redirects so deep links keep working.
+ */
+export function settingsRouteElements(): ReactNode {
+ return (
+ <>
+ } />
+
+ {/* ── General ─────────────────────────────────────────────── */}
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+ {/* Real device-pairing panel (replaces the old "Coming Soon" stub). */}
+ )} />
+
+ {/* ── Assistant ───────────────────────────────────────────── */}
+ {/* LLM / Voice / Embeddings moved to the Connections page. */}
+ } />
+ } />
+ )} />
+ } />
+ )} />
+ )} />
+ )} />
+ )} />
+ {/* Top-level agent profiles (soul, memory, skills, MCP, connectors). */}
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+
+ {/* ── Data ────────────────────────────────────────────────── */}
+ )} />
+ )} />
+ )} />
+
+ {/* ── Connections ─────────────────────────────────────────── */}
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+
+ {/* ── System ──────────────────────────────────────────────── */}
+ )} />
+ )} />
+
+ {/* ── Developer & Diagnostics leaf panels ─────────────────── */}
+ )}
+ />
+ )} />
+ )} />
+ {/* Search engine settings moved to the Connections page. */}
+ } />
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+ )}
+ />
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+ )} />
+ {/* Knowledge & Memory panels moved to the Brain page. */}
+ } />
+ } />
+ } />
+ } />
+ )} />
+ )} />
+
+ {/* ── Legacy slugs → redirects (deep-link compatibility) ──── */}
+ {/* Old hub pages */}
+ } />
+ } />
+ } />
+ } />
+ } />
+ {/* Composio (API key + routing) moved to Connections → API keys. */}
+ } />
+ {/* Merged Usage & Limits page */}
+ } />
+ } />
+ } />
+ {/* Autonomy rate-limit lives inside Agent access now */}
+ } />
+ {/* Merged Personality & Face page */}
+ } />
+ } />
+ {/* Merged Integrations page */}
+ } />
+ }
+ />
+ }
+ />
+ {/* Notification routing tab */}
+ }
+ />
+ {/* Fallback */}
+ } />
+ >
+ );
+}
diff --git a/app/src/components/skills/AutocompleteSetupModal.tsx b/app/src/components/skills/AutocompleteSetupModal.tsx
index c4cc0a5d3..4a6922091 100644
--- a/app/src/components/skills/AutocompleteSetupModal.tsx
+++ b/app/src/components/skills/AutocompleteSetupModal.tsx
@@ -6,7 +6,7 @@
* Intelligence setup modal.
*/
import { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router-dom';
import { useT } from '../../lib/i18n/I18nContext';
import { useCoreState } from '../../providers/CoreStateProvider';
@@ -14,6 +14,7 @@ import {
openhumanAutocompleteSetStyle,
openhumanAutocompleteStart,
} from '../../utils/tauriCommands/autocomplete';
+import { settingsNavState } from '../settings/modal/settingsOverlay';
import {
SetupNotice,
SetupSettingRow,
@@ -29,6 +30,7 @@ interface Props {
export default function AutocompleteSetupModal({ onClose }: Props) {
const navigate = useNavigate();
+ const location = useLocation();
const { t } = useT();
const { snapshot, refresh } = useCoreState();
const status = snapshot.runtime.autocomplete;
@@ -58,7 +60,7 @@ export default function AutocompleteSetupModal({ onClose }: Props) {
const handleGoToSettings = () => {
onClose();
- navigate('/settings/autocomplete');
+ navigate('/settings/autocomplete', settingsNavState(location));
};
return (
diff --git a/app/src/components/skills/ScreenIntelligenceSetupModal.tsx b/app/src/components/skills/ScreenIntelligenceSetupModal.tsx
index e75e054b9..f1d7077f9 100644
--- a/app/src/components/skills/ScreenIntelligenceSetupModal.tsx
+++ b/app/src/components/skills/ScreenIntelligenceSetupModal.tsx
@@ -6,11 +6,12 @@
* skill setup flows (Gmail, etc.).
*/
import { useEffect, useMemo, useState } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router-dom';
import { useScreenIntelligenceState } from '../../features/screen-intelligence/useScreenIntelligenceState';
import { useT } from '../../lib/i18n/I18nContext';
import { openhumanUpdateScreenIntelligenceSettings } from '../../utils/tauriCommands';
+import { settingsNavState } from '../settings/modal/settingsOverlay';
import { CheckIcon } from '../ui';
import {
SetupNotice,
@@ -90,6 +91,7 @@ const PermissionRow = ({
export default function ScreenIntelligenceSetupModal({ onClose, initialStep }: Props) {
const navigate = useNavigate();
+ const location = useLocation();
const { t } = useT();
const {
status,
@@ -151,7 +153,7 @@ export default function ScreenIntelligenceSetupModal({ onClose, initialStep }: P
const handleGoToSettings = () => {
onClose();
- navigate('/settings/screen-intelligence');
+ navigate('/settings/screen-intelligence', settingsNavState(location));
};
if (status?.platform_supported === false) {
diff --git a/app/src/components/skills/VoiceSetupModal.tsx b/app/src/components/skills/VoiceSetupModal.tsx
index 3eadfdb2c..04f0adef0 100644
--- a/app/src/components/skills/VoiceSetupModal.tsx
+++ b/app/src/components/skills/VoiceSetupModal.tsx
@@ -5,7 +5,7 @@
* settings. Otherwise, starts the voice server and shows success.
*/
import { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router-dom';
import type { VoiceSkillStatus } from '../../features/voice/useVoiceSkillStatus';
import { useT } from '../../lib/i18n/I18nContext';
@@ -13,6 +13,7 @@ import {
openhumanUpdateVoiceServerSettings,
openhumanVoiceServerStart,
} from '../../utils/tauriCommands/voice';
+import { settingsNavState } from '../settings/modal/settingsOverlay';
import { CheckIcon, WarningIcon } from '../ui';
import {
SetupNotice,
@@ -30,6 +31,7 @@ interface Props {
export default function VoiceSetupModal({ onClose, skillStatus }: Props) {
const navigate = useNavigate();
+ const location = useLocation();
const { t } = useT();
const { sttModelMissing, serverStatus } = skillStatus;
@@ -57,12 +59,12 @@ export default function VoiceSetupModal({ onClose, skillStatus }: Props) {
onClose();
// STT model install lives on the Voice settings panel (PR 2). The
// legacy `/settings/local-model` route handled Ollama assets only.
- navigate('/settings/voice');
+ navigate('/settings/voice', settingsNavState(location));
};
const handleGoToSettings = () => {
onClose();
- navigate('/settings/voice');
+ navigate('/settings/voice', settingsNavState(location));
};
return (
diff --git a/app/src/components/skills/__tests__/ScreenIntelligenceSetupModal.test.tsx b/app/src/components/skills/__tests__/ScreenIntelligenceSetupModal.test.tsx
index 963c0d0c3..33c2ae4df 100644
--- a/app/src/components/skills/__tests__/ScreenIntelligenceSetupModal.test.tsx
+++ b/app/src/components/skills/__tests__/ScreenIntelligenceSetupModal.test.tsx
@@ -13,7 +13,11 @@ vi.mock('../../../features/screen-intelligence/useScreenIntelligenceState', () =
vi.mock('react-router-dom', async () => {
const actual = await vi.importActual>('react-router-dom');
- return { ...actual, useNavigate: vi.fn(() => vi.fn()) };
+ return {
+ ...actual,
+ useNavigate: vi.fn(() => vi.fn()),
+ useLocation: () => ({ pathname: '/connections', search: '', hash: '', state: null, key: 'test' }),
+ };
});
vi.mock('../../../utils/tauriCommands', async () => {
diff --git a/app/src/pages/Conversations.tsx b/app/src/pages/Conversations.tsx
index 3377868d4..e29992553 100644
--- a/app/src/pages/Conversations.tsx
+++ b/app/src/pages/Conversations.tsx
@@ -13,6 +13,7 @@ import ChatNewWindowHero from '../components/chat/ChatNewWindowHero';
import ComposerTokenStats from '../components/chat/ComposerTokenStats';
import { ConfirmationModal } from '../components/intelligence/ConfirmationModal';
import { SidebarContent } from '../components/layout/shell/SidebarSlot';
+import { settingsNavState } from '../components/settings/modal/settingsOverlay';
import UpsellBanner from '../components/upsell/UpsellBanner';
import { dismissBanner, shouldShowBanner } from '../components/upsell/upsellDismissState';
import MicComposer from '../features/human/MicComposer';
@@ -2431,7 +2432,7 @@ const Conversations = ({
// STT/TTS provider settings live on the Voice panel
// since PR 2; the legacy local-model route was for
// back when speech assets were lumped with Ollama.
- navigate('/settings/voice');
+ navigate('/settings/voice', settingsNavState(location));
}}
className="text-xs text-primary-500 hover:text-primary-600 font-medium transition-colors">
{t('chat.setup')}
diff --git a/app/src/pages/Rewards.tsx b/app/src/pages/Rewards.tsx
index c2ce51cc5..a0b0e8c59 100644
--- a/app/src/pages/Rewards.tsx
+++ b/app/src/pages/Rewards.tsx
@@ -1,12 +1,13 @@
import createDebug from 'debug';
import { useCallback, useEffect, useState } from 'react';
-import { useNavigate } from 'react-router-dom';
+import { useLocation, useNavigate } from 'react-router-dom';
import EmptyStateCard from '../components/EmptyStateCard';
import PillTabBar from '../components/PillTabBar';
import RewardsCommunityTab from '../components/rewards/RewardsCommunityTab';
import RewardsRedeemTab from '../components/rewards/RewardsRedeemTab';
import RewardsReferralsTab from '../components/rewards/RewardsReferralsTab';
+import { settingsNavState } from '../components/settings/modal/settingsOverlay';
import { useT } from '../lib/i18n/I18nContext';
import { useCoreState } from '../providers/CoreStateProvider';
import { rewardsApi } from '../services/api/rewardsApi';
@@ -30,6 +31,7 @@ function errorMessage(err: unknown): string {
const Rewards = () => {
const { t } = useT();
const navigate = useNavigate();
+ const location = useLocation();
const { snapshot: coreSnapshot } = useCoreState();
const isLocalSession = isLocalSessionToken(coreSnapshot.sessionToken);
const [selectedTab, setSelectedTab] = useState('rewards');
@@ -124,7 +126,7 @@ const Rewards = () => {
title={t('rewards.title')}
description={t('rewards.localUnavailable')}
actionLabel={t('rewards.localUnavailableCta')}
- onAction={() => navigate('/settings/account')}
+ onAction={() => navigate('/settings/account', settingsNavState(location))}
/>
diff --git a/app/src/pages/Settings.tsx b/app/src/pages/Settings.tsx
index 817c29e50..b02918a42 100644
--- a/app/src/pages/Settings.tsx
+++ b/app/src/pages/Settings.tsx
@@ -1,241 +1,27 @@
-import type { ReactNode } from 'react';
-import { Navigate, Route, Routes } from 'react-router-dom';
+import { Route, Routes } from 'react-router-dom';
-import WorkflowsTab from '../components/intelligence/WorkflowsTab';
-import SettingsIndexRedirect from '../components/settings/layout/SettingsIndexRedirect';
import SettingsLayout from '../components/settings/layout/SettingsLayout';
-import AboutPanel from '../components/settings/panels/AboutPanel';
-import AccountPanel from '../components/settings/panels/AccountPanel';
-import AgentAccessPanel from '../components/settings/panels/AgentAccessPanel';
-import AgentActivityPanel from '../components/settings/panels/AgentActivityPanel';
-import AgentBoxPanel from '../components/settings/panels/AgentBoxPanel';
-import AgentChatPanel from '../components/settings/panels/AgentChatPanel';
-import AgentEditorPage from '../components/settings/panels/AgentEditorPage';
-import AgentsPanel from '../components/settings/panels/AgentsPanel';
-import AppearancePanel from '../components/settings/panels/AppearancePanel';
-import ApprovalHistoryPanel from '../components/settings/panels/ApprovalHistoryPanel';
-import AutocompleteDebugPanel from '../components/settings/panels/AutocompleteDebugPanel';
-import AutocompletePanel from '../components/settings/panels/AutocompletePanel';
-import BillingPanel from '../components/settings/panels/BillingPanel';
-import CompanionPanel from '../components/settings/panels/CompanionPanel';
-import ComposioTriagePanel from '../components/settings/panels/ComposioTriagePanel';
-import CronJobsPanel from '../components/settings/panels/CronJobsPanel';
-import DesktopAgentPanel from '../components/settings/panels/DesktopAgentPanel';
-import DeveloperOptionsPanel from '../components/settings/panels/DeveloperOptionsPanel';
-import DevicesPanel from '../components/settings/panels/DevicesPanel';
-import DevWorkflowPanel from '../components/settings/panels/DevWorkflowPanel';
-import EventLogPanel from '../components/settings/panels/EventLogPanel';
-import IntegrationsPanel from '../components/settings/panels/IntegrationsPanel';
-import LocalModelDebugPanel from '../components/settings/panels/LocalModelDebugPanel';
-import McpServerPanel from '../components/settings/panels/McpServerPanel';
-import MeetingSettingsPanel from '../components/settings/panels/MeetingSettingsPanel';
-import MemorySyncPanel from '../components/settings/panels/MemorySyncPanel';
-import MigrationPanel from '../components/settings/panels/MigrationPanel';
-import ModelHealthPanel from '../components/settings/panels/ModelHealthPanel';
-import NotificationsTabbedPanel from '../components/settings/panels/NotificationsTabbedPanel';
-import PermissionsPanel from '../components/settings/panels/PermissionsPanel';
-import PersonalityPanel from '../components/settings/panels/PersonalityPanel';
-import PrivacyPanel from '../components/settings/panels/PrivacyPanel';
-import ProfileEditorPage from '../components/settings/panels/ProfileEditorPage';
-import ProfilesPanel from '../components/settings/panels/ProfilesPanel';
-import RecoveryPhrasePanel from '../components/settings/panels/RecoveryPhrasePanel';
-import SandboxSettingsPanel from '../components/settings/panels/SandboxSettingsPanel';
-import ScreenAwarenessDebugPanel from '../components/settings/panels/ScreenAwarenessDebugPanel';
-import ScreenIntelligencePanel from '../components/settings/panels/ScreenIntelligencePanel';
-import SecurityPanel from '../components/settings/panels/SecurityPanel';
-import TasksPanel from '../components/settings/panels/TasksPanel';
-import TeamInvitesPanel from '../components/settings/panels/TeamInvitesPanel';
-import TeamManagementPanel from '../components/settings/panels/TeamManagementPanel';
-import TeamMembersPanel from '../components/settings/panels/TeamMembersPanel';
-import TeamPanel from '../components/settings/panels/TeamPanel';
-import ToolPolicyDiagnosticsPanel from '../components/settings/panels/ToolPolicyDiagnosticsPanel';
-import ToolsPanel from '../components/settings/panels/ToolsPanel';
-import UsagePanel from '../components/settings/panels/UsagePanel';
-import VoiceDebugPanel from '../components/settings/panels/VoiceDebugPanel';
-import WalletBalancesPanel from '../components/settings/panels/WalletBalancesPanel';
-import WebhooksDebugPanel from '../components/settings/panels/WebhooksDebugPanel';
-import WorkflowRunnerPanel from '../components/settings/panels/WorkflowRunnerPanel';
-
-const WrappedSettingsPage = ({ children }: { children: ReactNode }) => {
- // The surrounding two-pane card (bg / border / rounding) is provided by
- // SettingsLayout's content pane, so panels sit directly on it. This wrapper
- // fills the bounded Outlet area and is the page's single vertical scroll
- // region: PanelScaffold-based panels are `h-full` and own their own internal
- // scroll (so this never scrolls for them), while legacy panels that overflow
- // scroll here. Either way there's exactly one scrollbar.
- return
{children}
;
-};
+import { settingsRouteElements } from '../components/settings/settingsRouteElements';
/**
- * Settings routes, hosted inside the two-pane SettingsLayout (persistent
- * sidebar on md+, drill-down on narrow viewports). Retired slugs are kept as
- * redirects so deep links keep working.
+ * Full-page Settings host. Used on iOS (and any non-desktop target) where
+ * Settings is a routed screen rather than the desktop modal overlay. Wraps the
+ * shared {@link settingsRouteElements} route table in the two-pane
+ * {@link SettingsLayout} (persistent sidebar on md+, drill-down on narrow
+ * viewports). Retired slugs are kept as redirects inside the shared table so
+ * deep links keep working.
+ *
+ * Desktop no longer mounts this — it renders the same route table inside
+ * `SettingsModal` (see components/settings/modal/). The route elements are
+ * shared so both hosts stay in lockstep.
*/
const Settings = () => {
- const wrapSettingsPage = (element: ReactNode) => (
- {element}
- );
-
return (
// h-full chains the AppShell page-scroller height down to SettingsLayout so
- // its panes can bound to the viewport (minus the bottom bar, via the
- // scroller's pb-16) and scroll internally — instead of the whole page
- // growing and scrolling as one.
+ // its panes can bound to the viewport and scroll internally.