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";...@@ -32,6 +32,7 @@ import { type Thread } from "../types";
32import { derivePendingApprovals } from "../session-logic";32import { derivePendingApprovals } from "../session-logic";
33import { gitRemoveWorktreeMutationOptions, gitStatusQueryOptions } from "../lib/gitReactQuery";33import { gitRemoveWorktreeMutationOptions, gitStatusQueryOptions } from "../lib/gitReactQuery";
34import { serverConfigQueryOptions } from "../lib/serverReactQuery";34import { serverConfigQueryOptions } from "../lib/serverReactQuery";
35import { useMediaQuery } from "../hooks/useMediaQuery";
35import { readNativeApi } from "../nativeApi";36import { readNativeApi } from "../nativeApi";
36import { type DraftThreadEnvMode, useComposerDraftStore } from "../composerDraftStore";37import { type DraftThreadEnvMode, useComposerDraftStore } from "../composerDraftStore";
37import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";38import { selectThreadTerminalState, useTerminalStateStore } from "../terminalStateStore";
...@@ -49,6 +50,7 @@ import {...@@ -49,6 +50,7 @@ import {
49} from "./desktopUpdate.logic";50} from "./desktopUpdate.logic";
50import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert";51import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert";
51import { Button } from "./ui/button";52import { Button } from "./ui/button";
53import { shouldOpenProjectFolderPickerImmediately } from "./Sidebar.logic";
52import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./ui/collapsible";54import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "./ui/collapsible";
53import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";55import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
54import {56import {
...@@ -266,6 +268,7 @@ function ProjectFavicon({ cwd }: { cwd: string }) {...@@ -266,6 +268,7 @@ function ProjectFavicon({ cwd }: { cwd: string }) {
266}268}
267269
268export default function Sidebar() {270export default function Sidebar() {
271 const isMobile = useMediaQuery("(max-width: 767px)");
269 const projects = useStore((store) => store.projects);272 const projects = useStore((store) => store.projects);
270 const threads = useStore((store) => store.threads);273 const threads = useStore((store) => store.threads);
271 const markThreadUnread = useStore((store) => store.markThreadUnread);274 const markThreadUnread = useStore((store) => store.markThreadUnread);
...@@ -312,6 +315,11 @@ export default function Sidebar() {...@@ -312,6 +315,11 @@ export default function Sidebar() {
312 const renamingCommittedRef = useRef(false);315 const renamingCommittedRef = useRef(false);
313 const renamingInputRef = useRef<HTMLInputElement | null>(null);316 const renamingInputRef = useRef<HTMLInputElement | null>(null);
314 const [desktopUpdateState, setDesktopUpdateState] = useState<DesktopUpdateState | null>(null);317 const [desktopUpdateState, setDesktopUpdateState] = useState<DesktopUpdateState | null>(null);
318 const shouldBrowseForProjectImmediately = shouldOpenProjectFolderPickerImmediately({
319 isElectron,
320 isMobile,
321 });
322 const shouldShowProjectPathEntry = addingProject && !shouldBrowseForProjectImmediately;
315 const pendingApprovalByThreadId = useMemo(() => {323 const pendingApprovalByThreadId = useMemo(() => {
316 const map = new Map<ThreadId, boolean>();324 const map = new Map<ThreadId, boolean>();
317 for (const thread of threads) {325 for (const thread of threads) {
...@@ -550,12 +558,21 @@ export default function Sidebar() {...@@ -550,12 +558,21 @@ export default function Sidebar() {
550 }558 }
551 if (pickedPath) {559 if (pickedPath) {
552 await addProjectFromPath(pickedPath);560 await addProjectFromPath(pickedPath);
553 } else {561 } else if (!shouldBrowseForProjectImmediately) {
554 addProjectInputRef.current?.focus();562 addProjectInputRef.current?.focus();
555 }563 }
556 setIsPickingFolder(false);564 setIsPickingFolder(false);
557 };565 };
558566
567 const handleStartAddProject = () => {
568 setAddProjectError(null);
569 if (shouldBrowseForProjectImmediately) {
570 void handlePickFolder();
571 return;
572 }
573 setAddingProject((prev) => !prev);
574 };
575
559 const cancelRename = useCallback(() => {576 const cancelRename = useCallback(() => {
560 setRenamingThreadId(null);577 setRenamingThreadId(null);
561 renamingInputRef.current = null;578 renamingInputRef.current = null;
...@@ -1075,21 +1092,23 @@ export default function Sidebar() {...@@ -1075,21 +1092,23 @@ export default function Sidebar() {
1075 <button1092 <button
1076 type="button"1093 type="button"
1077 aria-label="Add project"1094 aria-label="Add project"
1095 aria-pressed={shouldShowProjectPathEntry}
1078 className="inline-flex size-5 items-center justify-center rounded-md text-muted-foreground/60 transition-colors hover:bg-accent hover:text-foreground"1096 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={() => {1097 onClick={handleStartAddProject}
1080 setAddingProject((prev) => !prev);
1081 setAddProjectError(null);
1082 }}
1083 />1098 />
1084 }1099 }
1085 >1100 >
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 />
1087 </TooltipTrigger>1106 </TooltipTrigger>
1088 <TooltipPopup side="right">Add project</TooltipPopup>1107 <TooltipPopup side="right">Add project</TooltipPopup>
1089 </Tooltip>1108 </Tooltip>
1090 </div>1109 </div>
10911110
1092 {addingProject && (1111 {shouldShowProjectPathEntry && (
1093 <div className="mb-2 px-1">1112 <div className="mb-2 px-1">
1094 {isElectron && (1113 {isElectron && (
1095 <button1114 <button
...@@ -1415,7 +1434,7 @@ export default function Sidebar() {...@@ -1415,7 +1434,7 @@ export default function Sidebar() {
1415 })}1434 })}
1416 </SidebarMenu>1435 </SidebarMenu>
14171436
1418 {projects.length === 0 && !addingProject && (1437 {projects.length === 0 && !shouldShowProjectPathEntry && (
1419 <div className="px-2 pt-4 text-center text-xs text-muted-foreground/60">1438 <div className="px-2 pt-4 text-center text-xs text-muted-foreground/60">
1420 No projects yet1439 No projects yet
1421 </div>1440 </div>