authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-03-09 09:02:56-07:00
committergravatar for jmarminge@gmail.comJulius Marminge <jmarminge@gmail.com> 2026-03-09 09:31:39-07:00
log75ca0d4fa41a4b5a85a6aa852dad813fbcbb94db
treef17f267fa33b1dbbc55a97fa5e1d377e45593963
parent8282b11fb55e50147a33e476ee8563ceaf1e75d7

Open folder picker immediately on desktop Electron

- add `shouldOpenProjectFolderPickerImmediately` to centralize Sidebar behavior - trigger folder browse directly when adding a project in desktop Electron - keep manual path entry for mobile layouts and non-Electron, with tests for all cases

3 files changed, 64 insertions(+), 8 deletions(-)

apps/web/src/components/Sidebar.logic.test.ts created+31
......@@ -0,0 +1,31 @@
1import { describe, expect, it } from "vitest";
2import { shouldOpenProjectFolderPickerImmediately } from "./Sidebar.logic";
3
4describe("shouldOpenProjectFolderPickerImmediately", () => {
5 it("opens the folder picker immediately in Electron on desktop", () => {
6 expect(
7 shouldOpenProjectFolderPickerImmediately({
8 isElectron: true,
9 isMobile: false,
10 }),
11 ).toBe(true);
12 });
13
14 it("keeps manual project entry on mobile Electron layouts", () => {
15 expect(
16 shouldOpenProjectFolderPickerImmediately({
17 isElectron: true,
18 isMobile: true,
19 }),
20 ).toBe(false);
21 });
22
23 it("keeps manual project entry outside Electron", () => {
24 expect(
25 shouldOpenProjectFolderPickerImmediately({
26 isElectron: false,
27 isMobile: false,
28 }),
29 ).toBe(false);
30 });
31});
apps/web/src/components/Sidebar.logic.ts created+6
......@@ -0,0 +1,6 @@
1export function shouldOpenProjectFolderPickerImmediately(input: {
2 isElectron: boolean;
3 isMobile: boolean;
4}): boolean {
5 return input.isElectron && !input.isMobile;
6}
apps/web/src/components/Sidebar.tsx+27-8
......@@ -32,6 +32,7 @@ import { type Thread } from "../types";
3232import { derivePendingApprovals } from "../session-logic";
3333import { gitRemoveWorktreeMutationOptions, gitStatusQueryOptions } from "../lib/gitReactQuery";
3434import { serverConfigQueryOptions } from "../lib/serverReactQuery";
35import { useMediaQuery } from "../hooks/useMediaQuery";
3536import { readNativeApi } from "../nativeApi";
3637import { type DraftThreadEnvMode, useComposerDraftStore } from "../composerDraftStore";
3738import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";
......@@ -49,6 +50,7 @@ import {
4950} from "./desktopUpdate.logic";
5051import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert";
5152import { Button } from "./ui/button";
53import { shouldOpenProjectFolderPickerImmediately } from "./Sidebar.logic";
5254import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./ui/collapsible";
5355import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
5456import {
......@@ -266,6 +268,7 @@ function ProjectFavicon({ cwd }: { cwd: string }) {
266268}
267269
268270export default function Sidebar() {
271 const isMobile = useMediaQuery("(max-width: 767px)");
269272 const projects = useStore((store) => store.projects);
270273 const threads = useStore((store) => store.threads);
271274 const markThreadUnread = useStore((store) => store.markThreadUnread);
......@@ -312,6 +315,11 @@ export default function Sidebar() {
312315 const renamingCommittedRef = useRef(false);
313316 const renamingInputRef = useRef<HTMLInputElement | null>(null);
314317 const [desktopUpdateState, setDesktopUpdateState] = useState<DesktopUpdateState | null>(null);
318 const shouldBrowseForProjectImmediately = shouldOpenProjectFolderPickerImmediately({
319 isElectron,
320 isMobile,
321 });
322 const shouldShowProjectPathEntry = addingProject && !shouldBrowseForProjectImmediately;
315323 const pendingApprovalByThreadId = useMemo(() => {
316324 const map = new Map<ThreadId, boolean>();
317325 for (const thread of threads) {
......@@ -550,12 +558,21 @@ export default function Sidebar() {
550558 }
551559 if (pickedPath) {
552560 await addProjectFromPath(pickedPath);
553 } else {
561 } else if (!shouldBrowseForProjectImmediately) {
554562 addProjectInputRef.current?.focus();
555563 }
556564 setIsPickingFolder(false);
557565 };
558566
567 const handleStartAddProject = () => {
568 setAddProjectError(null);
569 if (shouldBrowseForProjectImmediately) {
570 void handlePickFolder();
571 return;
572 }
573 setAddingProject((prev) => !prev);
574 };
575
559576 const cancelRename = useCallback(() => {
560577 setRenamingThreadId(null);
561578 renamingInputRef.current = null;
......@@ -1075,21 +1092,23 @@ export default function Sidebar() {
10751092 <button
10761093 type="button"
10771094 aria-label="Add project"
1095 aria-pressed={shouldShowProjectPathEntry}
10781096 className="inline-flex size-5 items-center justify-center rounded-md text-muted-foreground/60 transition-colors hover:bg-accent hover:text-foreground"
1079 onClick={() => {
1080 setAddingProject((prev) => !prev);
1081 setAddProjectError(null);
1082 }}
1097 onClick={handleStartAddProject}
10831098 />
10841099 }
10851100 >
1086 <PlusIcon className="size-3.5" />
1101 <PlusIcon
1102 className={`size-3.5 transition-transform duration-150 ${
1103 shouldShowProjectPathEntry ? "rotate-45" : "rotate-0"
1104 }`}
1105 />
10871106 </TooltipTrigger>
10881107 <TooltipPopup side="right">Add project</TooltipPopup>
10891108 </Tooltip>
10901109 </div>
10911110
1092 {addingProject && (
1111 {shouldShowProjectPathEntry && (
10931112 <div className="mb-2 px-1">
10941113 {isElectron && (
10951114 <button
......@@ -1415,7 +1434,7 @@ export default function Sidebar() {
14151434 })}
14161435 </SidebarMenu>
14171436
1418 {projects.length === 0 && !addingProject && (
1437 {projects.length === 0 && !shouldShowProjectPathEntry && (
14191438 <div className="px-2 pt-4 text-center text-xs text-muted-foreground/60">
14201439 No projects yet
14211440 </div>