diff --git a/apps/server/src/routes/terminal/routes/settings.ts b/apps/server/src/routes/terminal/routes/settings.ts index f4d6c007..21151ee6 100644 --- a/apps/server/src/routes/terminal/routes/settings.ts +++ b/apps/server/src/routes/terminal/routes/settings.ts @@ -26,6 +26,13 @@ export function createSettingsUpdateHandler() { const { maxSessions } = req.body; if (typeof maxSessions === "number") { + if (!Number.isInteger(maxSessions)) { + res.status(400).json({ + success: false, + error: "maxSessions must be an integer", + }); + return; + } if (maxSessions < MIN_MAX_SESSIONS || maxSessions > MAX_MAX_SESSIONS) { res.status(400).json({ success: false, diff --git a/apps/ui/src/components/views/terminal-view.tsx b/apps/ui/src/components/views/terminal-view.tsx index 4d801e42..3559d6b7 100644 --- a/apps/ui/src/components/views/terminal-view.tsx +++ b/apps/ui/src/components/views/terminal-view.tsx @@ -245,7 +245,7 @@ export function TerminalView() { const lastCreateTimeRef = useRef(0); const isCreatingRef = useRef(false); const prevProjectPathRef = useRef(null); - const isRestoringLayoutRef = useRef(false); + const restoringProjectPathRef = useRef(null); const [newSessionIds, setNewSessionIds] = useState>(new Set()); const [serverSessionInfo, setServerSessionInfo] = useState<{ current: number; max: number } | null>(null); const hasShownHighRamWarningRef = useRef(false); @@ -438,11 +438,14 @@ export function TerminalView() { const currentPath = currentProject?.path || null; const prevPath = prevProjectPathRef.current; - // Skip if no change or if we're restoring layout - if (currentPath === prevPath || isRestoringLayoutRef.current) { + // Skip if no change + if (currentPath === prevPath) { return; } + // If we're restoring a different project, that restore will be stale - let it finish but ignore results + // The path check in restoreLayout will handle this + // Save layout for previous project (if there was one and has terminals) if (prevPath && terminalState.tabs.length > 0) { saveTerminalLayout(prevPath); @@ -462,13 +465,20 @@ export function TerminalView() { if (savedLayout && savedLayout.tabs.length > 0) { // Restore the saved layout - try to reconnect to existing sessions - isRestoringLayoutRef.current = true; + // Track which project we're restoring to detect stale restores + restoringProjectPathRef.current = currentPath; // Clear existing terminals first (only client state, sessions stay on server) clearTerminalState(); // Create terminals and build layout - try to reconnect or create new const restoreLayout = async () => { + // Check if we're still restoring the same project (user may have switched) + if (restoringProjectPathRef.current !== currentPath) { + console.log("[Terminal] Restore cancelled - project changed"); + return; + } + let failedSessions = 0; let totalSessions = 0; let reconnectedSessions = 0; @@ -578,6 +588,12 @@ export function TerminalView() { // For each saved tab, rebuild the layout for (let tabIndex = 0; tabIndex < savedLayout.tabs.length; tabIndex++) { + // Check if project changed during restore - bail out early + if (restoringProjectPathRef.current !== currentPath) { + console.log("[Terminal] Restore cancelled mid-loop - project changed"); + return; + } + const savedTab = savedLayout.tabs[tabIndex]; // Create the tab first @@ -619,7 +635,10 @@ export function TerminalView() { duration: 5000, }); } finally { - isRestoringLayoutRef.current = false; + // Only clear if we're still the active restore + if (restoringProjectPathRef.current === currentPath) { + restoringProjectPathRef.current = null; + } } }; @@ -631,7 +650,8 @@ export function TerminalView() { // Also save when tabs become empty so closed terminals stay closed on refresh const saveLayoutTimeoutRef = useRef(null); useEffect(() => { - if (currentProject?.path && !isRestoringLayoutRef.current) { + // Don't save while restoring this project's layout + if (currentProject?.path && restoringProjectPathRef.current !== currentProject.path) { // Debounce saves to prevent excessive localStorage writes during rapid changes if (saveLayoutTimeoutRef.current) { clearTimeout(saveLayoutTimeoutRef.current);