Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 58 additions & 74 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,31 +5,28 @@

import React, { Suspense, lazy, useState, useEffect } from 'react';
import { motion, AnimatePresence } from 'motion/react';
import { Menu, X as XIcon, Wifi } from 'lucide-react';
import { Wifi } from 'lucide-react';
import TopBar from './components/TopBar';
import Sidebar from './components/Sidebar';
import MainArea from './components/MainArea';
import NewsPopup, { shouldShowNewsPopup, dismissNewsPopup } from './components/NewsPopup';
import { AppProvider, useAppContext } from './AppContext';
import { isVideoPath, toFileUrl } from './media';
import UpdateModal from './components/UpdateModal';
import { VoiceIndicator, VoiceInstallModal } from './components/VoiceSettings';
import { syncWakeMode } from './voice';
import { useCompactLayout } from './useCompactLayout';

// Modals and widgets load on demand to keep startup fast.
const SettingsModal = lazy(() => import('./components/SettingsModal'));
const WelcomePopup = lazy(() => import('./components/WelcomePopup'));
const SpotifyWidget = lazy(() => import('./components/SpotifyWidget'));
const SetupModal = lazy(() => import('./components/SetupModal'));
const MobileConnectModal = lazy(() => import('./components/MobileConnectModal'));

const WELCOME_POPUP_STORAGE_KEY = 'agentWorkspace.welcomePopupDismissed';
const SIDEBAR_STORAGE_KEY = 'agentWorkspace.sidebarCollapsed';

function AppLayout() {
const {
theme,
customThemes,
hasSetupCompleted,
sidebarTransparency,
surfaceTransparency,
spotifyWidgetEnabled,
Expand All @@ -41,35 +38,44 @@ function AppLayout() {
mobileConnectionConfig,
setMobileConnectionConfig,
setMobileMode,
mainView,
selectedChatId,
selectedProject,
} = useAppContext();
const [showSettings, setShowSettings] = useState(false);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const [showMobileConnect, setShowMobileConnect] = useState(false);
const [showWelcomePopup, setShowWelcomePopup] = useState(() => {
const compact = useCompactLayout() || mobileMode;
// On phones and tablets the sidebar is a drawer that starts closed.
const [drawerOpen, setDrawerOpen] = useState(false);
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
try {
return localStorage.getItem(WELCOME_POPUP_STORAGE_KEY) !== 'true';
return localStorage.getItem(SIDEBAR_STORAGE_KEY) === 'true';
} catch {
return true;
return false;
}
});
const [showNewsPopup, setShowNewsPopup] = useState(shouldShowNewsPopup);

const closeNewsPopup = () => {
setShowNewsPopup(false);
dismissNewsPopup();
const sidebarOpen = compact ? drawerOpen : !sidebarCollapsed;
const toggleSidebar = () => {
if (compact) return setDrawerOpen((open) => !open);
setSidebarCollapsed((collapsed) => {
try {
localStorage.setItem(SIDEBAR_STORAGE_KEY, String(!collapsed));
} catch {
// localStorage may be unavailable in restricted render contexts.
}
return !collapsed;
});
};
const [showMobileConnect, setShowMobileConnect] = useState(false);
// Close the drawer after navigating somewhere from it.
useEffect(() => {
setDrawerOpen(false);
}, [mainView, selectedChatId, selectedProject?.id, compact]);

// Start the always-on wake word listener if it is enabled.
useEffect(() => {
void syncWakeMode();
}, []);

// Re-check news popup when setup completes later in the session
useEffect(() => {
if (hasSetupCompleted && shouldShowNewsPopup()) {
setShowNewsPopup(true);
}
}, [hasSetupCompleted]);
const selectedCustomTheme = customThemes.find((item) => item.id === theme);
// Media-only themes keep the look of their base theme and only swap the background.
const baseThemeId = selectedCustomTheme?.mediaOnly ? selectedCustomTheme.baseTheme || 'modern-dark' : theme;
Expand All @@ -79,15 +85,6 @@ function AppLayout() {
const backdropTheme = selectedCustomTheme?.mediaOnly ? selectedCustomTheme : customTheme;
const cssTheme = customTheme ? 'custom' : baseThemeId;

const closeWelcomePopup = () => {
setShowWelcomePopup(false);
try {
localStorage.setItem(WELCOME_POPUP_STORAGE_KEY, 'true');
} catch {
// localStorage may be unavailable in restricted render contexts.
}
};

const bgClass = {
'modern-dark': 'bg-gradient-to-br from-[#1c181a] via-[#111111] to-[#0a0a0a]',
'classic-light': 'bg-gradient-to-br from-[#f5f2ed] via-[#e5e1d8] to-[#d1ccc0]',
Expand Down Expand Up @@ -190,51 +187,41 @@ function AppLayout() {
>
{backdropTheme && <ThemeBackdrop theme={backdropTheme} />}
<div className="relative z-10 flex h-full w-full flex-col">
<TopBar onSettingsClick={() => setShowSettings(true)} />
<TopBar
onSettingsClick={() => setShowSettings(true)}
sidebarOpen={sidebarOpen}
onToggleSidebar={toggleSidebar}
/>
<div className="flex flex-1 overflow-hidden relative">
{/* Mobile Mode: Overlay sidebar toggle button */}
{mobileMode && (
<button
onClick={() => setMobileSidebarOpen((v) => !v)}
className="fixed left-3 top-14 z-50 flex h-10 w-10 items-center justify-center rounded-xl border border-white/10 bg-[#1c181a]/90 backdrop-blur-md text-white shadow-2xl hover:bg-white/10 transition-all duration-200"
title={mobileSidebarOpen ? 'Seitenleiste schliessen' : 'Seitenleiste oeffnen'}
>
{mobileSidebarOpen ? <XIcon className="w-5 h-5" /> : <Menu className="w-5 h-5" />}
</button>
{compact ? (
<AnimatePresence>
{drawerOpen && (
<>
<motion.div
className="absolute inset-0 z-30 bg-black/60 backdrop-blur-sm"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setDrawerOpen(false)}
/>
<motion.div
className="absolute inset-y-0 left-0 z-40 max-w-[85vw] shadow-2xl"
initial={{ x: '-100%' }}
animate={{ x: 0 }}
exit={{ x: '-100%' }}
transition={{ type: 'tween', duration: 0.2 }}
>
<Sidebar onSettingsClick={() => setShowSettings(true)} />
</motion.div>
</>
)}
</AnimatePresence>
) : (
sidebarOpen && <Sidebar onSettingsClick={() => setShowSettings(true)} />
)}
{/* Sidebar: hidden when mobile mode is on and sidebar is collapsed */}
<div
className={`${
isMobileMode
? mobileSidebarOpen
? 'fixed inset-0 z-40 w-full lg:static lg:w-[270px]'
: 'hidden lg:block lg:w-[270px]'
: 'lg:w-[270px]'
} shrink-0`}
>
{isMobileMode && mobileSidebarOpen && (
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm z-30 lg:hidden"
onClick={() => setMobileSidebarOpen(false)}
/>
)}
<div className={`relative z-40 h-full ${isMobileMode ? 'max-w-[300px] shadow-2xl' : ''}`}>
<Sidebar onSettingsClick={() => setShowSettings(true)} />
</div>
</div>
<MainArea />
</div>
<Suspense fallback={null}>
<AnimatePresence>
{hasSetupCompleted && showNewsPopup && (
<NewsPopup onClose={closeNewsPopup} />
)}
</AnimatePresence>
<AnimatePresence>
{hasSetupCompleted && !showNewsPopup && showWelcomePopup && (
<WelcomePopup onClose={closeWelcomePopup} />
)}
</AnimatePresence>
<AnimatePresence>
{spotifyWidgetEnabled && <SpotifyWidget />}
</AnimatePresence>
Expand All @@ -244,9 +231,6 @@ function AppLayout() {
<UpdateModal />
<VoiceInstallModal />
<VoiceIndicator />
<AnimatePresence>
{!hasSetupCompleted && <SetupModal />}
</AnimatePresence>

{/* Mobile Mode: Connect modal */}
<AnimatePresence>
Expand Down
19 changes: 19 additions & 0 deletions src/AppContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -890,6 +890,25 @@ export function AppProvider({ children }: { children: ReactNode }) {
useEffect(() => writeStorage('terminalStartCommand', terminalStartCommand), [terminalStartCommand]);
useEffect(() => writeStorage('cliHarnesses', cliHarnesses), [cliHarnesses]);
useEffect(() => writeStorage('agyAccounts', agyAccounts), [agyAccounts]);
// CodeForge Web: the accounts live on the server (shared with the codeforge CLI), not in this browser.
const agyAccountsLoaded = useRef(false);
useEffect(() => {
const workspace = window.agentWorkspace;
if (!workspace?.isWeb || !workspace.listAgyAccounts) return;
workspace
.listAgyAccounts()
.then((accounts) => {
agyAccountsLoaded.current = true;
setAgyAccounts(accounts);
})
.catch(() => {});
}, []);
useEffect(() => {
const workspace = window.agentWorkspace;
if (!workspace?.isWeb || !workspace.saveAgyAccounts || !agyAccountsLoaded.current) return;
const timer = setTimeout(() => void workspace.saveAgyAccounts?.(agyAccounts).catch(() => {}), 500);
return () => clearTimeout(timer);
}, [agyAccounts]);
useEffect(() => writeStorage('terminalPrefixSuffixEnabled', terminalPrefixSuffixEnabled), [terminalPrefixSuffixEnabled]);
useEffect(() => writeStorage('terminalPrefix', terminalPrefix), [terminalPrefix]);
useEffect(() => writeStorage('terminalSuffix', terminalSuffix), [terminalSuffix]);
Expand Down
12 changes: 4 additions & 8 deletions src/components/AccountMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, type ReactNode } from 'react';
import { BookOpen, CircleUserRound, Loader2, LogOut, Megaphone, MessageSquare, Play, Settings } from 'lucide-react';
import { PROVIDER_MODELS, useAppContext } from '../AppContext';
import TutorialModal from './TutorialModal';
import NewsPopup from './NewsPopup';

export default function AccountMenu({
onSettingsClick,
Expand All @@ -26,6 +27,7 @@ export default function AccountMenu({
const [busy, setBusy] = useState<'limit' | null>(null);
const [status, setStatus] = useState('');
const [showTutorial, setShowTutorial] = useState(false);
const [showNews, setShowNews] = useState(false);
const providerInfo = PROVIDER_INFO[provider];

return (
Expand Down Expand Up @@ -139,14 +141,7 @@ export default function AccountMenu({
<AccountMenuButton
icon={<Megaphone className="h-4 w-4" />}
label="Was ist neu"
onClick={() => {
if (window.confirm('Seite neu laden um das News-Popup erneut anzuzeigen?')) {
try {
localStorage.removeItem('agentWorkspace.newsPopupDismissed');
} catch {}
window.location.reload();
}
}}
onClick={() => setShowNews(true)}
/>
<AccountMenuButton
icon={<LogOut className="h-4 w-4" />}
Expand All @@ -159,6 +154,7 @@ export default function AccountMenu({
</div>
</div>
{showTutorial && <TutorialModal onClose={() => setShowTutorial(false)} />}
{showNews && <NewsPopup onClose={() => setShowNews(false)} />}
</>
);
}
Expand Down
Loading
Loading