Skip to content
Merged
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
14 changes: 7 additions & 7 deletions src/browser/components/LeftSidebar/LeftSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
};
Expand Down Expand Up @@ -77,7 +77,7 @@ export function LeftSidebar(props: LeftSidebarProps) {
onToggleCollapsed={onToggleCollapsed}
/>

{!collapsed && !isMobileTouch && onStartResize && (
{!collapsed && !isMobileOverlay && onStartResize && (
<div
data-testid="left-sidebar-resize-handle"
className={cn(
Expand Down
45 changes: 36 additions & 9 deletions src/browser/components/ProjectCreateModal/ProjectCreateModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ interface ProjectCreateFormProps {
placeholder?: string;
/** Hide the footer actions (submit/cancel buttons). */
hideFooter?: boolean;
onErrorChange?: (hasError: boolean) => void;
}

export interface ProjectCreateFormHandle {
Expand All @@ -118,14 +119,23 @@ export const ProjectCreateForm = React.forwardRef<ProjectCreateFormHandle, Proje
? "C:\\Users\\user\\projects\\my-project"
: "/home/user/projects/my-project",
hideFooter = false,
onErrorChange,
},
ref
) {
const { api } = useAPI();
const [path, setPath] = useState(initialPath ?? "");
const [error, setError] = useState("");
const [error, setErrorState] = useState("");
const [isCreating, setIsCreating] = useState(false);

const setError = useCallback(
(next: string) => {
setErrorState(next);
onErrorChange?.(next.length > 0);
},
[onErrorChange]
);

useEffect(() => {
setPath(initialPath ?? "");
}, [initialPath]);
Expand All @@ -141,7 +151,7 @@ export const ProjectCreateForm = React.forwardRef<ProjectCreateFormHandle, Proje
const reset = useCallback(() => {
setPath("");
setError("");
}, []);
}, [setError]);

const handleCancel = useCallback(() => {
reset();
Expand Down Expand Up @@ -211,7 +221,7 @@ export const ProjectCreateForm = React.forwardRef<ProjectCreateFormHandle, Proje
} finally {
setCreating(false);
}
}, [api, isCreating, onClose, onSuccess, path, reset, setCreating]);
}, [api, isCreating, onClose, onSuccess, path, reset, setCreating, setError]);

const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
Expand Down Expand Up @@ -297,6 +307,7 @@ interface ProjectCloneFormProps {
onIsCreatingChange?: (isCreating: boolean) => void;
hideFooter?: boolean;
autoFocus?: boolean;
onErrorChange?: (hasError: boolean) => void;
}

export interface ProjectCloneFormHandle {
Expand Down Expand Up @@ -375,7 +386,16 @@ const ProjectCloneForm = React.forwardRef<ProjectCloneFormHandle, ProjectCloneFo
const [repoUrl, setRepoUrl] = useState("");
const [cloneParentDir, setCloneParentDir] = useState(props.defaultProjectDir);
const [hasEditedCloneParentDir, setHasEditedCloneParentDir] = useState(false);
const [error, setError] = useState("");
const [error, setErrorState] = useState("");

const onErrorChange = props.onErrorChange;
const setError = useCallback(
(next: string) => {
setErrorState(next);
onErrorChange?.(next.length > 0);
},
[onErrorChange]
);
const [destinationExistsPath, setDestinationExistsPath] = useState<string | null>(null);
const [isCreating, setIsCreating] = useState(false);
const [cloneOutput, setCloneOutput] = useState("");
Expand All @@ -402,7 +422,7 @@ const ProjectCloneForm = React.forwardRef<ProjectCloneFormHandle, ProjectCloneFo
rawOutputRef.current = "";
setDestinationExistsPath(null);
setIsAddingProject(false);
}, [props.defaultProjectDir]);
}, [props.defaultProjectDir, setError]);

const abortInFlightClone = useCallback(() => {
if (!abortControllerRef.current) {
Expand Down Expand Up @@ -552,7 +572,7 @@ const ProjectCloneForm = React.forwardRef<ProjectCloneFormHandle, ProjectCloneFo
setCreating(false);
}
}
}, [api, isCreating, props, repoUrl, reset, setCreating, trimmedCloneParentDir]);
}, [api, isCreating, props, repoUrl, reset, setCreating, setError, trimmedCloneParentDir]);

const handleAddExistingProject = useCallback(async () => {
if (!api || !destinationExistsPath) {
Expand Down Expand Up @@ -593,13 +613,13 @@ const ProjectCloneForm = React.forwardRef<ProjectCloneFormHandle, ProjectCloneFo
setIsAddingProject(false);
}
}
}, [api, destinationExistsPath, props, reset]);
}, [api, destinationExistsPath, props, reset, setError]);

const handleRetry = useCallback(() => {
setError("");
setCloneOutput("");
rawOutputRef.current = "";
}, []);
}, [setError]);

const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
Expand Down Expand Up @@ -794,6 +814,7 @@ interface ProjectAddFormProps {
autoFocus?: boolean;
hideFooter?: boolean;
showCancelButton?: boolean;
onErrorChange?: (hasError: boolean) => void;
}

export const ProjectAddForm = React.forwardRef<ProjectAddFormHandle, ProjectAddFormProps>(
Expand Down Expand Up @@ -864,18 +885,22 @@ export const ProjectAddForm = React.forwardRef<ProjectAddFormHandle, ProjectAddF
void ensureDefaultCloneDir();
}, [ensureDefaultCloneDir, mode, props.isOpen]);

const onErrorChange = props.onErrorChange;
const handleModeChange = useCallback(
(nextMode: string) => {
if (nextMode !== "pick-folder" && nextMode !== "clone") {
return;
}

setMode(nextMode);
// The newly mounted form starts without an error, so clear any stale flag
// reported by the form we're switching away from.
onErrorChange?.(false);
if (nextMode === "clone") {
void ensureDefaultCloneDir();
}
},
[ensureDefaultCloneDir]
[ensureDefaultCloneDir, onErrorChange]
);

useImperativeHandle(
Expand Down Expand Up @@ -930,6 +955,7 @@ export const ProjectAddForm = React.forwardRef<ProjectAddFormHandle, ProjectAddF
showCancelButton={props.showCancelButton ?? false}
autoFocus={props.autoFocus}
onIsCreatingChange={setCreating}
onErrorChange={props.onErrorChange}
hideFooter
/>
) : (
Expand All @@ -940,6 +966,7 @@ export const ProjectAddForm = React.forwardRef<ProjectAddFormHandle, ProjectAddF
isOpen={props.isOpen}
defaultProjectDir={defaultProjectDir}
onIsCreatingChange={setCreating}
onErrorChange={props.onErrorChange}
hideFooter
autoFocus={props.autoFocus}
/>
Expand Down
20 changes: 15 additions & 5 deletions src/browser/features/SplashScreens/OnboardingWizardSplash.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {

const projectAddFormRef = useRef<ProjectAddFormHandle | null>(null);
const [isProjectCreating, setIsProjectCreating] = useState(false);
const [projectFormHasError, setProjectFormHasError] = useState(false);

const [direction, setDirection] = useState<Direction>("forward");

Expand Down Expand Up @@ -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);
Expand All @@ -852,11 +854,14 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
/>
</div>

<p className="mt-2 text-xs">
{userProjects.size > 0
? "Add another folder or repo, or leave this blank and click Next to continue."
: "Click Next to add this project."}
</p>
{/* Hide the "click Next" nudge while the form shows an error; the two contradict. */}
{!projectFormHasError && (
Comment thread
ibetitsmike marked this conversation as resolved.
<p className="mt-2 text-xs">
{userProjects.size > 0
? "Add another folder or repo, or leave this blank and click Next to continue."
: "Click Next to add this project."}
</p>
)}
</>
),
});
Expand Down Expand Up @@ -1011,6 +1016,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
muxGatewayLoginInProgress,
muxGatewayLoginStatus,
openProvidersSettings,
projectFormHasError,
userProjects.size,
providersConfig,
refreshMuxGatewayAccountStatus,
Expand Down Expand Up @@ -1048,6 +1054,9 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
return;
}
setDirection("back");
// Changing steps remounts the project form without its inline error, so
// clear the stale flag or the "click Next" nudge stays hidden on return.
setProjectFormHasError(false);
setStepIndex((i) => Math.max(0, i - 1));
};

Expand All @@ -1056,6 +1065,7 @@ export function OnboardingWizardSplash(props: { onDismiss: () => void }) {
return;
}
setDirection("forward");
setProjectFormHasError(false);
setStepIndex((i) => Math.min(totalSteps - 1, i + 1));
};

Expand Down
11 changes: 9 additions & 2 deletions src/browser/features/SplashScreens/SplashScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,18 @@ export function SplashScreen(props: SplashScreenProps) {

return (
<Dialog open onOpenChange={(open) => !open && props.onDismiss()}>
<DialogContent maxWidth="500px" onInteractOutside={(e) => e.preventDefault()}>
{/* aria-describedby={undefined}: splash bodies are rich content, not a short description. */}
{/* Cap height and scroll the body (footer pinned) so tall steps keep Next/Skip clickable. */}
<DialogContent
maxWidth="500px"
aria-describedby={undefined}
className="max-h-[85vh] grid-rows-[auto_minmax(0,1fr)_auto]"
onInteractOutside={(e) => e.preventDefault()}
>
<DialogHeader>
<DialogTitle>{props.title}</DialogTitle>
</DialogHeader>
{props.children}
<div className="min-h-0 overflow-y-auto">{props.children}</div>
<DialogFooter className={props.footerClassName}>
{props.footer ?? (
<>
Expand Down
72 changes: 36 additions & 36 deletions src/browser/styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -1140,7 +1140,40 @@ body,
min-width: 44px;
}

/* Show mobile menu button only on touch devices */
/* Keep workspace header visible when keyboard opens - fixed positioning
because iOS Safari scrolls the visual viewport, not a scroll container.
Left/right safe-area insets ensure header content avoids notch in landscape.
min-height 44px for touch targets, but allow growth for wrapped controls. */
.mobile-sticky-header {
position: fixed !important;
top: env(safe-area-inset-top, 0px) !important;
left: env(safe-area-inset-left, 0px) !important;
right: env(safe-area-inset-right, 0px) !important;
z-index: 50 !important;
min-height: 44px !important;
height: auto !important;
flex-wrap: wrap !important;
}

/* Add padding to content below fixed header so it's not hidden underneath.
Use generous padding (88px = 2x touch targets) to accommodate header wrapping.
A true dynamic solution would require JS to measure header height. */
.mobile-header-spacer {
padding-top: 88px !important;
}

/* Hide right sidebar on mobile - too narrow for useful interaction */
.mobile-hide-right-sidebar {
display: none !important;
}
}

/* Tailwind utility extensions for dark theme surfaces */

/* Narrow viewports, any pointer type: the expanded sidebar overlays the main
pane instead of crushing it (a 288px in-flow sidebar leaves ~90px of content
at 375px), and the menu button provides a way to reopen it once hidden. */
@media (max-width: 768px) {
.mobile-menu-btn {
display: inline-flex !important;
justify-content: flex-start !important;
Expand All @@ -1151,12 +1184,10 @@ body,
padding-left: 0 !important;
}

/* Show mobile overlay only on touch devices */
.mobile-overlay {
display: block !important;
}

/* Mobile sidebar positioning only on touch devices */
.mobile-sidebar {
position: fixed !important;
left: 0 !important;
Expand All @@ -1177,7 +1208,7 @@ body,
box-shadow: none !important;
}

/* Mobile layout - stack vertically on touch devices */
/* Stack vertically so main content gets the full viewport width */
.mobile-layout {
flex-direction: column !important;
}
Expand All @@ -1186,38 +1217,7 @@ body,
width: 100% !important;
}

/* Keep workspace header visible when keyboard opens - fixed positioning
because iOS Safari scrolls the visual viewport, not a scroll container.
Left/right safe-area insets ensure header content avoids notch in landscape.
min-height 44px for touch targets, but allow growth for wrapped controls. */
.mobile-sticky-header {
position: fixed !important;
top: env(safe-area-inset-top, 0px) !important;
left: env(safe-area-inset-left, 0px) !important;
right: env(safe-area-inset-right, 0px) !important;
z-index: 50 !important;
min-height: 44px !important;
height: auto !important;
flex-wrap: wrap !important;
}

/* Add padding to content below fixed header so it's not hidden underneath.
Use generous padding (88px = 2x touch targets) to accommodate header wrapping.
A true dynamic solution would require JS to measure header height. */
.mobile-header-spacer {
padding-top: 88px !important;
}

/* Hide right sidebar on mobile - too narrow for useful interaction */
.mobile-hide-right-sidebar {
display: none !important;
}
}

/* Tailwind utility extensions for dark theme surfaces */

/* Hide right sidebar on small screens so it never stacks below the chat pane. */
@media (max-width: 768px) {
/* Hide right sidebar on small screens so it never stacks below the chat pane. */
.mobile-hide-right-sidebar {
display: none !important;
}
Expand Down
Loading
Loading