mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +00:00
fix(ui): restore startup project context
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -95,3 +95,4 @@ check-sync.sh
|
|||||||
# API key files
|
# API key files
|
||||||
data/.api-key
|
data/.api-key
|
||||||
data/credentials.json
|
data/credentials.json
|
||||||
|
data/
|
||||||
|
|||||||
@@ -517,8 +517,9 @@ export function hydrateStoreFromSettings(settings: GlobalSettings): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Save theme to localStorage for fallback when server settings aren't available
|
// Save theme to localStorage for fallback when server settings aren't available
|
||||||
if (settings.theme) {
|
const storedTheme = (currentProject?.theme as string | undefined) || settings.theme;
|
||||||
setItem(THEME_STORAGE_KEY, settings.theme);
|
if (storedTheme) {
|
||||||
|
setItem(THEME_STORAGE_KEY, storedTheme);
|
||||||
}
|
}
|
||||||
|
|
||||||
useAppStore.setState({
|
useAppStore.setState({
|
||||||
|
|||||||
@@ -43,6 +43,12 @@ const NO_STORE_CACHE_MODE: RequestCache = 'no-store';
|
|||||||
const AUTO_OPEN_HISTORY_INDEX = 0;
|
const AUTO_OPEN_HISTORY_INDEX = 0;
|
||||||
const SINGLE_PROJECT_COUNT = 1;
|
const SINGLE_PROJECT_COUNT = 1;
|
||||||
const DEFAULT_LAST_OPENED_TIME_MS = 0;
|
const DEFAULT_LAST_OPENED_TIME_MS = 0;
|
||||||
|
const AUTO_OPEN_STATUS = {
|
||||||
|
idle: 'idle',
|
||||||
|
opening: 'opening',
|
||||||
|
done: 'done',
|
||||||
|
} as const;
|
||||||
|
type AutoOpenStatus = (typeof AUTO_OPEN_STATUS)[keyof typeof AUTO_OPEN_STATUS];
|
||||||
|
|
||||||
// Apply stored theme immediately on page load (before React hydration)
|
// Apply stored theme immediately on page load (before React hydration)
|
||||||
// This prevents flash of default theme on login/setup pages
|
// This prevents flash of default theme on login/setup pages
|
||||||
@@ -160,6 +166,7 @@ function RootLayoutContent() {
|
|||||||
const [streamerPanelOpen, setStreamerPanelOpen] = useState(false);
|
const [streamerPanelOpen, setStreamerPanelOpen] = useState(false);
|
||||||
const authChecked = useAuthStore((s) => s.authChecked);
|
const authChecked = useAuthStore((s) => s.authChecked);
|
||||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||||
|
const settingsLoaded = useAuthStore((s) => s.settingsLoaded);
|
||||||
const { openFileBrowser } = useFileBrowser();
|
const { openFileBrowser } = useFileBrowser();
|
||||||
|
|
||||||
// Load project settings when switching projects
|
// Load project settings when switching projects
|
||||||
@@ -171,6 +178,20 @@ function RootLayoutContent() {
|
|||||||
const isDashboardRoute = location.pathname === '/dashboard';
|
const isDashboardRoute = location.pathname === '/dashboard';
|
||||||
const isBoardRoute = location.pathname === '/board';
|
const isBoardRoute = location.pathname === '/board';
|
||||||
const isRootRoute = location.pathname === '/';
|
const isRootRoute = location.pathname === '/';
|
||||||
|
const [autoOpenStatus, setAutoOpenStatus] = useState<AutoOpenStatus>(AUTO_OPEN_STATUS.idle);
|
||||||
|
const autoOpenCandidate = selectAutoOpenProject(currentProject, projects, projectHistory);
|
||||||
|
const canAutoOpen =
|
||||||
|
authChecked &&
|
||||||
|
isAuthenticated &&
|
||||||
|
settingsLoaded &&
|
||||||
|
setupComplete &&
|
||||||
|
!isLoginRoute &&
|
||||||
|
!isLoggedOutRoute &&
|
||||||
|
!isSetupRoute &&
|
||||||
|
!!autoOpenCandidate;
|
||||||
|
const shouldAutoOpen = canAutoOpen && autoOpenStatus !== AUTO_OPEN_STATUS.done;
|
||||||
|
const shouldBlockForSettings =
|
||||||
|
authChecked && isAuthenticated && !settingsLoaded && !isLoginRoute && !isLoggedOutRoute;
|
||||||
|
|
||||||
// Sandbox environment check state
|
// Sandbox environment check state
|
||||||
type SandboxStatus = 'pending' | 'containerized' | 'needs-confirmation' | 'denied' | 'confirmed';
|
type SandboxStatus = 'pending' | 'containerized' | 'needs-confirmation' | 'denied' | 'confirmed';
|
||||||
@@ -298,7 +319,6 @@ function RootLayoutContent() {
|
|||||||
|
|
||||||
// Ref to prevent concurrent auth checks from running
|
// Ref to prevent concurrent auth checks from running
|
||||||
const authCheckRunning = useRef(false);
|
const authCheckRunning = useRef(false);
|
||||||
const autoOpenAttemptedRef = useRef(false);
|
|
||||||
|
|
||||||
// Global listener for 401/403 responses during normal app usage.
|
// Global listener for 401/403 responses during normal app usage.
|
||||||
// This is triggered by the HTTP client whenever an authenticated request returns 401/403.
|
// This is triggered by the HTTP client whenever an authenticated request returns 401/403.
|
||||||
@@ -479,9 +499,6 @@ function RootLayoutContent() {
|
|||||||
// Note: Settings are now loaded in __root.tsx after successful session verification
|
// Note: Settings are now loaded in __root.tsx after successful session verification
|
||||||
// This ensures a unified flow across all modes (Electron, web, external server)
|
// This ensures a unified flow across all modes (Electron, web, external server)
|
||||||
|
|
||||||
// Get settingsLoaded from auth store for routing decisions
|
|
||||||
const settingsLoaded = useAuthStore((s) => s.settingsLoaded);
|
|
||||||
|
|
||||||
// Routing rules (ALL modes - unified flow):
|
// Routing rules (ALL modes - unified flow):
|
||||||
// - If not authenticated: force /logged-out (even /setup is protected)
|
// - If not authenticated: force /logged-out (even /setup is protected)
|
||||||
// - If authenticated but setup incomplete: force /setup
|
// - If authenticated but setup incomplete: force /setup
|
||||||
@@ -603,6 +620,9 @@ function RootLayoutContent() {
|
|||||||
// Redirect from welcome page based on project state
|
// Redirect from welcome page based on project state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMounted && isRootRoute) {
|
if (isMounted && isRootRoute) {
|
||||||
|
if (!settingsLoaded || shouldAutoOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (currentProject) {
|
if (currentProject) {
|
||||||
// Project is selected, go to board
|
// Project is selected, go to board
|
||||||
navigate({ to: '/board' });
|
navigate({ to: '/board' });
|
||||||
@@ -611,56 +631,58 @@ function RootLayoutContent() {
|
|||||||
navigate({ to: '/dashboard' });
|
navigate({ to: '/dashboard' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [isMounted, currentProject, isRootRoute, navigate]);
|
}, [isMounted, currentProject, isRootRoute, navigate, shouldAutoOpen, settingsLoaded]);
|
||||||
|
|
||||||
// Auto-open the most recent project on startup
|
// Auto-open the most recent project on startup
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (autoOpenAttemptedRef.current) return;
|
if (!canAutoOpen) return;
|
||||||
if (!authChecked || !isAuthenticated || !settingsLoaded) return;
|
if (autoOpenStatus !== AUTO_OPEN_STATUS.idle) return;
|
||||||
if (!setupComplete) return;
|
|
||||||
if (isLoginRoute || isLoggedOutRoute || isSetupRoute) return;
|
|
||||||
if (isBoardRoute) return;
|
|
||||||
|
|
||||||
const projectToOpen = selectAutoOpenProject(currentProject, projects, projectHistory);
|
if (!autoOpenCandidate) return;
|
||||||
if (!projectToOpen) return;
|
|
||||||
|
|
||||||
autoOpenAttemptedRef.current = true;
|
setAutoOpenStatus(AUTO_OPEN_STATUS.opening);
|
||||||
|
|
||||||
const openProject = async () => {
|
const openProject = async () => {
|
||||||
const initResult = await initializeProject(projectToOpen.path);
|
try {
|
||||||
if (!initResult.success) {
|
const initResult = await initializeProject(autoOpenCandidate.path);
|
||||||
logger.warn('Auto-open project failed:', initResult.error);
|
if (!initResult.success) {
|
||||||
|
logger.warn('Auto-open project failed:', initResult.error);
|
||||||
|
if (isRootRoute) {
|
||||||
|
navigate({ to: '/dashboard' });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentProject || currentProject.id !== autoOpenCandidate.id) {
|
||||||
|
upsertAndSetCurrentProject(
|
||||||
|
autoOpenCandidate.path,
|
||||||
|
autoOpenCandidate.name,
|
||||||
|
autoOpenCandidate.theme
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRootRoute) {
|
||||||
|
navigate({ to: '/board' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Auto-open project crashed:', error);
|
||||||
if (isRootRoute) {
|
if (isRootRoute) {
|
||||||
navigate({ to: '/dashboard' });
|
navigate({ to: '/dashboard' });
|
||||||
}
|
}
|
||||||
return;
|
} finally {
|
||||||
}
|
setAutoOpenStatus(AUTO_OPEN_STATUS.done);
|
||||||
|
|
||||||
if (!currentProject || currentProject.id !== projectToOpen.id) {
|
|
||||||
upsertAndSetCurrentProject(projectToOpen.path, projectToOpen.name, projectToOpen.theme);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isBoardRoute) {
|
|
||||||
navigate({ to: '/board' });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
void openProject();
|
void openProject();
|
||||||
}, [
|
}, [
|
||||||
authChecked,
|
canAutoOpen,
|
||||||
isAuthenticated,
|
autoOpenStatus,
|
||||||
settingsLoaded,
|
autoOpenCandidate,
|
||||||
setupComplete,
|
|
||||||
isLoginRoute,
|
|
||||||
isLoggedOutRoute,
|
|
||||||
isSetupRoute,
|
|
||||||
isBoardRoute,
|
|
||||||
isRootRoute,
|
|
||||||
currentProject,
|
currentProject,
|
||||||
projects,
|
|
||||||
projectHistory,
|
|
||||||
navigate,
|
navigate,
|
||||||
upsertAndSetCurrentProject,
|
upsertAndSetCurrentProject,
|
||||||
|
isRootRoute,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Bootstrap Codex models on app startup (after auth completes)
|
// Bootstrap Codex models on app startup (after auth completes)
|
||||||
@@ -736,6 +758,22 @@ function RootLayoutContent() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldBlockForSettings) {
|
||||||
|
return (
|
||||||
|
<main className="flex h-screen items-center justify-center" data-testid="app-container">
|
||||||
|
<LoadingState message="Loading settings..." />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldAutoOpen) {
|
||||||
|
return (
|
||||||
|
<main className="flex h-screen items-center justify-center" data-testid="app-container">
|
||||||
|
<LoadingState message="Opening project..." />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Show setup page (full screen, no sidebar) - authenticated only
|
// Show setup page (full screen, no sidebar) - authenticated only
|
||||||
if (isSetupRoute) {
|
if (isSetupRoute) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -114,6 +114,12 @@ function saveThemeToStorage(theme: ThemeMode): void {
|
|||||||
setItem(THEME_STORAGE_KEY, theme);
|
setItem(THEME_STORAGE_KEY, theme);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function persistEffectiveThemeForProject(project: Project | null, fallbackTheme: ThemeMode): void {
|
||||||
|
const projectTheme = project?.theme as ThemeMode | undefined;
|
||||||
|
const themeToStore = projectTheme ?? fallbackTheme;
|
||||||
|
saveThemeToStorage(themeToStore);
|
||||||
|
}
|
||||||
|
|
||||||
export type BoardViewMode = 'kanban' | 'graph';
|
export type BoardViewMode = 'kanban' | 'graph';
|
||||||
|
|
||||||
export interface ApiKeys {
|
export interface ApiKeys {
|
||||||
@@ -1241,13 +1247,16 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const isCurrent = get().currentProject?.id === projectId;
|
const isCurrent = get().currentProject?.id === projectId;
|
||||||
|
const nextCurrentProject = isCurrent ? null : get().currentProject;
|
||||||
|
|
||||||
set({
|
set({
|
||||||
projects: remainingProjects,
|
projects: remainingProjects,
|
||||||
trashedProjects: [trashedProject, ...existingTrash],
|
trashedProjects: [trashedProject, ...existingTrash],
|
||||||
currentProject: isCurrent ? null : get().currentProject,
|
currentProject: nextCurrentProject,
|
||||||
currentView: isCurrent ? 'welcome' : get().currentView,
|
currentView: isCurrent ? 'welcome' : get().currentView,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
persistEffectiveThemeForProject(nextCurrentProject, get().theme);
|
||||||
},
|
},
|
||||||
|
|
||||||
restoreTrashedProject: (projectId) => {
|
restoreTrashedProject: (projectId) => {
|
||||||
@@ -1266,6 +1275,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
|||||||
currentProject: samePathProject,
|
currentProject: samePathProject,
|
||||||
currentView: 'board',
|
currentView: 'board',
|
||||||
});
|
});
|
||||||
|
persistEffectiveThemeForProject(samePathProject, get().theme);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1283,6 +1293,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
|||||||
currentProject: restoredProject,
|
currentProject: restoredProject,
|
||||||
currentView: 'board',
|
currentView: 'board',
|
||||||
});
|
});
|
||||||
|
persistEffectiveThemeForProject(restoredProject, get().theme);
|
||||||
},
|
},
|
||||||
|
|
||||||
deleteTrashedProject: (projectId) => {
|
deleteTrashedProject: (projectId) => {
|
||||||
@@ -1302,6 +1313,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
|||||||
|
|
||||||
setCurrentProject: (project) => {
|
setCurrentProject: (project) => {
|
||||||
set({ currentProject: project });
|
set({ currentProject: project });
|
||||||
|
persistEffectiveThemeForProject(project, get().theme);
|
||||||
if (project) {
|
if (project) {
|
||||||
set({ currentView: 'board' });
|
set({ currentView: 'board' });
|
||||||
// Add to project history (MRU order)
|
// Add to project history (MRU order)
|
||||||
@@ -1385,6 +1397,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
|||||||
projectHistoryIndex: newIndex,
|
projectHistoryIndex: newIndex,
|
||||||
currentView: 'board',
|
currentView: 'board',
|
||||||
});
|
});
|
||||||
|
persistEffectiveThemeForProject(targetProject, get().theme);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1418,6 +1431,7 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
|||||||
projectHistoryIndex: newIndex,
|
projectHistoryIndex: newIndex,
|
||||||
currentView: 'board',
|
currentView: 'board',
|
||||||
});
|
});
|
||||||
|
persistEffectiveThemeForProject(targetProject, get().theme);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -1477,12 +1491,14 @@ export const useAppStore = create<AppState & AppActions>()((set, get) => ({
|
|||||||
// Also update currentProject if it's the same project
|
// Also update currentProject if it's the same project
|
||||||
const currentProject = get().currentProject;
|
const currentProject = get().currentProject;
|
||||||
if (currentProject?.id === projectId) {
|
if (currentProject?.id === projectId) {
|
||||||
|
const updatedTheme = theme === null ? undefined : theme;
|
||||||
set({
|
set({
|
||||||
currentProject: {
|
currentProject: {
|
||||||
...currentProject,
|
...currentProject,
|
||||||
theme: theme === null ? undefined : theme,
|
theme: updatedTheme,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
persistEffectiveThemeForProject({ ...currentProject, theme: updatedTheme }, get().theme);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,10 @@ import { routeTree } from '../routeTree.gen';
|
|||||||
|
|
||||||
// Use browser history in web mode (for e2e tests and dev), memory history in Electron
|
// Use browser history in web mode (for e2e tests and dev), memory history in Electron
|
||||||
const isElectron = typeof window !== 'undefined' && window.electronAPI !== undefined;
|
const isElectron = typeof window !== 'undefined' && window.electronAPI !== undefined;
|
||||||
|
const BOARD_ROUTE_PATH = '/board';
|
||||||
|
|
||||||
const history = isElectron
|
const history = isElectron
|
||||||
? createMemoryHistory({ initialEntries: [window.location.pathname || '/'] })
|
? createMemoryHistory({ initialEntries: [BOARD_ROUTE_PATH] })
|
||||||
: createBrowserHistory();
|
: createBrowserHistory();
|
||||||
|
|
||||||
export const router = createRouter({
|
export const router = createRouter({
|
||||||
|
|||||||
Reference in New Issue
Block a user