diff --git a/app/src/App.tsx b/app/src/App.tsx index f4b42bad9..6bf335a54 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -13,12 +13,14 @@ import AppRoutes from './AppRoutes'; import AppBackground from './components/AppBackground'; import AppUpdatePrompt from './components/AppUpdatePrompt'; import BootCheckGate from './components/BootCheckGate/BootCheckGate'; -import BottomTabBar from './components/BottomTabBar'; import CommandProvider from './components/commands/CommandProvider'; import ServiceBlockingGate from './components/daemon/ServiceBlockingGate'; import DictationHotkeyManager from './components/DictationHotkeyManager'; import ErrorFallbackScreen from './components/ErrorFallbackScreen'; import KeyringConsentOverlay from './components/keyring/KeyringConsentOverlay'; +import AppSidebar from './components/layout/shell/AppSidebar'; +import RootShellLayout from './components/layout/shell/RootShellLayout'; +import { SidebarSlotProvider } from './components/layout/shell/SidebarSlot'; import LocalAIDownloadSnackbar from './components/LocalAIDownloadSnackbar'; import SecretPromptDialog from './components/mcp-setup/SecretPromptDialog'; import OpenhumanLinkModal from './components/OpenhumanLinkModal'; @@ -50,12 +52,13 @@ import { stopInternetStatusListener, } from './services/internetStatusListener'; import { + hideWebviewAccount, startWebviewAccountService, stopWebviewAccountService, } from './services/webviewAccountService'; import { persistor, store } from './store'; import { useAppSelector } from './store/hooks'; -import { isAccountsFullscreen } from './utils/accountsFullscreen'; +import { AGENT_ACCOUNT_ID } from './utils/accountsFullscreen'; import { DEV_FORCE_ONBOARDING } from './utils/config'; // Attach the `webview:event` listener at app boot so background recipe @@ -162,11 +165,6 @@ function AppShellDesktop() { const location = useLocation(); const navigate = useNavigate(); const { snapshot, isBootstrapping } = useCoreState(); - const activeAccountId = useAppSelector(state => state.accounts.activeAccountId); - // On /accounts, only the agent view keeps the tab bar + its reserved - // bottom padding. Any other selected "app" (e.g. WhatsApp) takes the - // full viewport so the embedded webview goes edge-to-edge. - const fullscreen = isAccountsFullscreen(location.pathname, activeAccountId); const onOnboardingRoute = location.pathname.startsWith('/onboarding'); const onboardingPending = !!snapshot.sessionToken && (DEV_FORCE_ONBOARDING || !snapshot.onboardingCompleted); @@ -183,9 +181,9 @@ function AppShellDesktop() { navigate('/onboarding', { replace: true }); } else if (!onboardingPending && onOnboardingRoute) { console.debug( - `[onboarding-gate] redirecting ${location.pathname} -> /home (onboarding complete)` + `[onboarding-gate] redirecting ${location.pathname} -> /chat (onboarding complete)` ); - navigate('/home', { replace: true }); + navigate('/chat', { replace: true }); } }, [ isBootstrapping, @@ -201,6 +199,17 @@ function AppShellDesktop() { trackPageView(location.pathname); }, [location.pathname]); + // Hide the active connected-app webview when we navigate away from the chat + // surface. The native CEF webview composites above the HTML, so without this + // it lingers on top of the newly-routed page until the user returns. + const activeAccountId = useAppSelector(state => state.accounts.activeAccountId); + useEffect(() => { + const onChat = location.pathname === '/chat' || location.pathname.startsWith('/chat/'); + if (!onChat && activeAccountId && activeAccountId !== AGENT_ACCOUNT_ID) { + void hideWebviewAccount(activeAccountId); + } + }, [location.pathname, activeAccountId]); + // Sync the notch indicator to the persisted always-on listening state once // the core is ready (once per boot). Extracted to a hook so it's testable. useNotchBootSync(isBootstrapping); @@ -214,32 +223,48 @@ function AppShellDesktop() { } }, [location.pathname, navType]); - return ( -
+ {statusCopy} +
+ + {/* Recovery: only when the local core is the broken link. */} + {blocking === 'core-unreachable' && ( +{restartError}
+ )} +- {t('home.banners.promoCreditsTitle').replace('{amount}', formatUsd(promoCredits))} -
-- {t('home.banners.promoCreditsBody')}{' '} - {' '} - {t('home.banners.promoCreditsUsage')} -
-+ {(() => { + // Single {amount} template; split so the amount renders bold inline. + const [before, after] = t('home.banners.promoCreditsBody').split('{amount}'); + return ( + <> + {before} + + {formatUsd(promoCredits)} + + {after} + > + ); + })()}{' '} + {' '} + {t('home.banners.promoCreditsUsage')} +