From c3df875b3bff859bff4b0f9749719ee6228eeac1 Mon Sep 17 00:00:00 2001
From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com>
Date: Tue, 14 Jul 2026 00:30:41 +0000
Subject: [PATCH 1/4] fix: dogfood onboarding + narrow-viewport fixes
- SplashScreen: cap dialog height (85vh) with scrollable body so tall wizard
steps keep Next/Skip clickable at short viewports; declare no dialog
description (aria-describedby={undefined}) to fix Radix a11y warning
- ProjectService.create: map EACCES/EPERM/EROFS/ENOTDIR mkdir/stat failures
to friendly errors instead of leaking raw Node errno strings
- ProjectAddForm/wizard: report inline error state upward and hide the
'Click Next to add this project.' nudge while an error is shown
- Sidebar overlay now gates on width (max-width: 768px) instead of width +
coarse pointer, so a narrow non-touch window no longer crushes main content
---
.../components/LeftSidebar/LeftSidebar.tsx | 14 ++--
.../ProjectCreateModal/ProjectCreateModal.tsx | 46 +++++++++---
.../SplashScreens/OnboardingWizardSplash.tsx | 16 +++--
.../features/SplashScreens/SplashScreen.tsx | 11 ++-
src/browser/styles/globals.css | 72 +++++++++----------
src/node/services/projectService.ts | 34 ++++++++-
tests/ipc/projects/create.test.ts | 55 ++++++++++++++
7 files changed, 188 insertions(+), 60 deletions(-)
diff --git a/src/browser/components/LeftSidebar/LeftSidebar.tsx b/src/browser/components/LeftSidebar/LeftSidebar.tsx
index a0080b5910..debdae22cd 100644
--- a/src/browser/components/LeftSidebar/LeftSidebar.tsx
+++ b/src/browser/components/LeftSidebar/LeftSidebar.tsx
@@ -26,15 +26,15 @@ export function LeftSidebar(props: LeftSidebarProps) {
...projectSidebarProps
} = props;
const isDesktop = isDesktopMode();
- // Match the CSS gate for the mobile "overlay" sidebar; we don't show a drag handle in that mode.
- const isMobileTouch =
- typeof window !== "undefined" &&
- window.matchMedia("(max-width: 768px) and (pointer: coarse)").matches;
+ // Match the CSS gate for the mobile "overlay" sidebar (width-only, any pointer
+ // type); we don't show a drag handle in that mode since CSS pins the width.
+ const isMobileOverlay =
+ typeof window !== "undefined" && window.matchMedia("(max-width: 768px)").matches;
const handleBeforeOpenSettings = () => {
- // Keep settings navigation escapable on touch devices by dismissing the
+ // Keep settings navigation escapable on narrow viewports by dismissing the
// off-canvas sidebar as soon as the user opens settings from this sidebar.
- if (!collapsed && isMobileTouch) {
+ if (!collapsed && isMobileOverlay) {
onToggleCollapsed();
}
};
@@ -77,7 +77,7 @@ export function LeftSidebar(props: LeftSidebarProps) {
onToggleCollapsed={onToggleCollapsed}
/>
- {!collapsed && !isMobileTouch && onStartResize && (
+ {!collapsed && !isMobileOverlay && onStartResize && (
void;
}
export interface ProjectCreateFormHandle {
@@ -118,14 +120,23 @@ export const ProjectCreateForm = React.forwardRef
{
+ setErrorState(next);
+ onErrorChange?.(next.length > 0);
+ },
+ [onErrorChange]
+ );
+
useEffect(() => {
setPath(initialPath ?? "");
}, [initialPath]);
@@ -141,7 +152,7 @@ export const ProjectCreateForm = React.forwardRef {
setPath("");
setError("");
- }, []);
+ }, [setError]);
const handleCancel = useCallback(() => {
reset();
@@ -211,7 +222,7 @@ export const ProjectCreateForm = React.forwardRef {
@@ -297,6 +308,8 @@ interface ProjectCloneFormProps {
onIsCreatingChange?: (isCreating: boolean) => void;
hideFooter?: boolean;
autoFocus?: boolean;
+ /** Notifies the parent when an inline error appears/clears (e.g. to hide contradictory helper text). */
+ onErrorChange?: (hasError: boolean) => void;
}
export interface ProjectCloneFormHandle {
@@ -375,7 +388,15 @@ const ProjectCloneForm = React.forwardRef {
+ setErrorState(next);
+ props.onErrorChange?.(next.length > 0);
+ },
+ [props]
+ );
const [destinationExistsPath, setDestinationExistsPath] = useState(null);
const [isCreating, setIsCreating] = useState(false);
const [cloneOutput, setCloneOutput] = useState("");
@@ -402,7 +423,7 @@ const ProjectCloneForm = React.forwardRef {
if (!abortControllerRef.current) {
@@ -552,7 +573,7 @@ const ProjectCloneForm = React.forwardRef {
if (!api || !destinationExistsPath) {
@@ -593,13 +614,13 @@ const ProjectCloneForm = React.forwardRef {
setError("");
setCloneOutput("");
rawOutputRef.current = "";
- }, []);
+ }, [setError]);
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
@@ -794,6 +815,8 @@ interface ProjectAddFormProps {
autoFocus?: boolean;
hideFooter?: boolean;
showCancelButton?: boolean;
+ /** Notifies the parent when the active form shows an inline error (e.g. to hide contradictory helper text). */
+ onErrorChange?: (hasError: boolean) => void;
}
export const ProjectAddForm = React.forwardRef(
@@ -871,11 +894,14 @@ export const ProjectAddForm = React.forwardRef
) : (
@@ -940,6 +967,7 @@ export const ProjectAddForm = React.forwardRef
diff --git a/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx b/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx
index 11baee93f8..0930316e12 100644
--- a/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx
+++ b/src/browser/features/SplashScreens/OnboardingWizardSplash.tsx
@@ -210,6 +210,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
const projectAddFormRef = useRef(null);
const [isProjectCreating, setIsProjectCreating] = useState(false);
+ const [projectFormHasError, setProjectFormHasError] = useState(false);
const [direction, setDirection] = useState("forward");
@@ -843,6 +844,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
autoFocus={userProjects.size === 0}
hideFooter
onIsCreatingChange={setIsProjectCreating}
+ onErrorChange={setProjectFormHasError}
onSuccess={(normalizedPath, projectConfig) => {
addProject(normalizedPath, projectConfig);
updatePersistedState(getAgentsInitNudgeKey(normalizedPath), true);
@@ -852,11 +854,14 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
/>
-
- {userProjects.size > 0
- ? "Add another folder or repo, or leave this blank and click Next to continue."
- : "Click Next to add this project."}
-
+ {/* Hide the "click Next" nudge while the form shows an error; the two contradict. */}
+ {!projectFormHasError && (
+
+ {userProjects.size > 0
+ ? "Add another folder or repo, or leave this blank and click Next to continue."
+ : "Click Next to add this project."}
+
+ )}
>
),
});
@@ -1011,6 +1016,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
muxGatewayLoginInProgress,
muxGatewayLoginStatus,
openProvidersSettings,
+ projectFormHasError,
userProjects.size,
providersConfig,
refreshMuxGatewayAccountStatus,
diff --git a/src/browser/features/SplashScreens/SplashScreen.tsx b/src/browser/features/SplashScreens/SplashScreen.tsx
index 8b73b57491..0ba4827a4d 100644
--- a/src/browser/features/SplashScreens/SplashScreen.tsx
+++ b/src/browser/features/SplashScreens/SplashScreen.tsx
@@ -42,11 +42,18 @@ export function SplashScreen(props: SplashScreenProps) {
return (