Skip to content

feat(desktop): support multi-folder selection in Open Project dialog #1248

Merged
Kitenite merged 2 commits into
superset-sh:mainfrom
quuu:main
Feb 7, 2026
Merged

feat(desktop): support multi-folder selection in Open Project dialog #1248
Kitenite merged 2 commits into
superset-sh:mainfrom
quuu:main

Conversation

@quuu
Copy link
Copy Markdown
Contributor

@quuu quuu commented Feb 6, 2026

Submitting this PR because I was trying out superset but ran into friction when I had to import my projects 1 by 1 via the native file selector

This PR enables me to select multiple folders to all upload as projects

Video below

CleanShot.2026-02-05.at.20.55.37.mp4

Note: All of this was written by Opencode Opus4.6, but I reviewed and tested the changes

Summary

  • Enable selecting multiple folders in the native "Open Project" file picker dialog
  • Each folder is processed independently with its own success/error/needsGitInit outcome
  • Show a summary toast (e.g. "3 projects opened") after multi-select
  • Navigate to the first successfully opened project

Changes

Core: Multi-select backend (projects.ts)

  • Added multiSelections to dialog.showOpenDialog properties
  • New FolderOutcome and OpenNewMultiResult types for per-folder results
  • openNew mutation now iterates all selected paths, catching errors per-folder instead of failing entirely

UI: All 4 call sites updated

  • StartView — summary toast + navigate to first project + InitGitDialog for non-git folders
  • WorkspaceSidebarFooter — creates branch workspaces for each project + summary toast
  • NewWorkspaceModal — imports all projects, selects first one + summary toast
  • InitGitDialog — now accepts multiple paths, displays scrollable list, batch-initializes git

Dev experience fixes (incidental)

  • SKIP_ENV_VALIDATION bypass — was broken because isPending guard and sign-in page redirected before the bypass could take effect
  • getGitHubUsername caching — added 5-minute in-memory cache to avoid GitHub API rate limit spam during dev

Testing

  1. Click "Open Project" → native file picker now allows multi-select (Cmd/Shift+click)
  2. Select 2+ git repos → toast shows "N projects opened", navigates to first
  3. Select mix of git repos + non-git folders → git repos open, InitGitDialog prompts for the rest
  4. Select from sidebar "Add repository" → same behavior with workspace creation

Summary by CodeRabbit

Release Notes

  • New Features

    • Import and open multiple projects simultaneously with individual success/failure feedback and detailed status updates for each project
    • Batch Git repository initialization supporting coordinated setup and configuration across multiple project folders with progress tracking and feedback
  • Improvements

    • Comprehensive per-project error reporting with detailed failure messages and actionable resolution guidance
    • Enhanced authentication performance through optimized caching

quuu added 2 commits February 5, 2026 20:59
Add multiSelections to the native file picker so users can select
multiple folders at once. Each folder is processed independently
with per-folder success/error/needsGitInit outcomes.

- Add FolderOutcome and OpenNewMultiResult types for multi-select results
- Update openNew tRPC mutation to iterate all selected paths
- Update all 4 UI call sites (StartView, WorkspaceSidebarFooter,
  NewWorkspaceModal, SidebarDropZone) to handle multi-results
- Update InitGitDialog to display and process multiple non-git folders
- Show summary toast (e.g. '3 projects opened') after multi-select
- Navigate to first successfully opened project from StartView
- Cache getGitHubUsername to avoid GitHub API rate limit spam
- Fix SKIP_ENV_VALIDATION dev bypass not working (isPending guard
  and sign-in page were redirecting before bypass could take effect)
feat(desktop): support multi-folder selection in Open Project dialog
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 6, 2026

📝 Walkthrough

Walkthrough

Multi-select support is introduced for opening projects. The openNew mutation now handles multiple folder paths, returning per-folder outcomes (success, needsGitInit, or error). UI components are updated to process multi-result data, showing aggregate toasts and handling per-item errors. GitHub username caching is added, and sign-in bypass via environment flag is implemented.

Changes

Cohort / File(s) Summary
Multi-select project opening
apps/desktop/src/lib/trpc/routers/projects/projects.ts
Converts openNew mutation to multi-path flow. Introduces FolderOutcome type with status variants (success, needsGitInit, error). Returns OpenNewMultiResult with aggregated per-folder outcomes. Iterates over selected paths, handling git initialization and workspace setup per-path.
Multi-result UI handling - Import
apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx
Enhances handleImportRepo to process multi-result outcomes. Displays per-project error toasts, aggregate success toast, and git-init warnings for non-git folders. Auto-selects first successful project.
Multi-result UI handling - Workspace
apps/desktop/src/renderer/screens/main/components/StartView/index.tsx, apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarFooter.tsx
Both components updated to handle multi-result project openings. Show aggregate toast summaries, per-item error toasts, create branch workspaces per successful project, and aggregate git-init warnings.
Multi-path git initialization
apps/desktop/src/renderer/screens/main/components/StartView/InitGitDialog.tsx
Adds selectedPaths prop and multi-path support. Iterates over paths for git initialization and workspace creation. Collects per-path errors and displays combined error summary. Updates UI text (titles, button labels) for multi-path scenarios.
GitHub username caching
apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts
Introduces in-memory cache for getGitHubUsername with 5-minute TTL. Returns cached value if valid; updates cache on successful or failed API calls before returning result.
Development/auth flow
apps/desktop/src/renderer/routes/_authenticated/layout.tsx, apps/desktop/src/renderer/routes/sign-in/page.tsx
Adds env.SKIP_ENV_VALIDATION bypass logic. Layout only renders pending spinner when flag is false. Sign-in page returns early to /workspace when flag is true, bypassing authentication UI.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as UI (StartView)
    participant Router as Backend (openNew)
    participant GitCheck as Git Operations
    participant Workspace as Workspace Creation
    participant Dialog as InitGitDialog

    User->>UI: Select multiple folders
    UI->>Router: Call openNew with multiple paths
    
    Router->>GitCheck: Check each path for git root
    
    alt Path has git repository
        GitCheck-->>Router: Return git root
        Router->>Workspace: Upsert project, setup workspace
        Workspace-->>Router: Success with Project object
        Router->>Router: Record FolderOutcome (success)
    else Path is not a git repository
        GitCheck-->>Router: Error/no git root
        Router->>Router: Record FolderOutcome (needsGitInit)
    else Unexpected error
        Router->>Router: Record FolderOutcome (error)
    end
    
    Router-->>UI: Return OpenNewMultiResult with outcomes array
    
    UI->>UI: Process results - separate success/needsGitInit/error
    
    alt Has successful projects
        UI->>User: Show success toast with count
        UI->>UI: Navigate to first successful project
    end
    
    alt Has error outcomes
        UI->>User: Show per-item error toasts
    end
    
    alt Has needsGitInit outcomes
        UI->>Dialog: Open with selected paths requiring init
        Dialog->>GitCheck: Initialize git for each path
        Dialog->>Workspace: Create workspace for each path
        Dialog-->>UI: Complete
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 Hop through the folders, one by one,
Select them all—the work is fun!
Multi-results dancing in the light,
Git or not, we get it right,
Cache the names, then off we go—
Many projects in a row! 🌱

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main feature added: multi-folder selection support in the Open Project dialog, which aligns with the primary objective of the changeset.
Description check ✅ Passed The description provides a clear summary, related context, type of change (new feature), testing steps, and additional technical notes. It covers all major template sections and explains the changes comprehensively.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/desktop/src/renderer/screens/main/components/StartView/InitGitDialog.tsx (1)

118-161: ⚠️ Potential issue | 🟡 Minor

Multi-path init flow is solid; minor UX concern with error visibility.

The sequential processing with per-path error collection is the right approach for git init + DB operations. One note: onClose() on Line 155 always fires (even when errors occurred), and in StartView the resulting error banner auto-dismisses after 5 seconds. For a batch of failures, the aggregated error message on Line 151 could be long and disappear before the user reads it. Consider showing errors via toast.error (which persists until dismissed) instead of the auto-dismissing banner, or letting the caller decide.

🤖 Fix all issues with AI agents
In `@apps/desktop/src/lib/trpc/routers/projects/projects.ts`:
- Around line 509-520: The catch block for the open-new-project flow logs the
real error but pushes a generic message into the outcomes array; update the
outcomes.push call in the catch to include the actual error text (e.g., derive
message via error instanceof Error ? error.message : String(error)) instead of
the static "Failed to open project" string so selectedPath and the real error
message are returned; locate the catch handling around the selectedPath variable
in the function (the section that logs "[projects/openNew] Failed to open
project") and replace the pushed error value accordingly.

In `@apps/desktop/src/renderer/screens/main/components/StartView/index.tsx`:
- Around line 60-108: The code currently calls navigate(...) when there are
successes and also sets setInitGitDialog(...) when needsGitInit exists, causing
StartView (which renders InitGitDialog) to unmount and the dialog to be lost;
fix by deferring navigation whenever needsGitInit.length > 0: detect
needsGitInit first, call setInitGitDialog({ isOpen: true, selectedPath: ...,
selectedPaths: ... }) and store the intended navigation target (e.g., in a
pendingNavigate state or include it on the dialog state), then only call
navigate(...) after the dialog is dismissed/handled; update InitGitDialog’s
confirm/close handlers to consume that pendingNavigate and perform navigate(...)
(use the existing setInitGitDialog, InitGitDialog component, and navigate
function names to locate and implement the change).
🧹 Nitpick comments (4)
apps/desktop/src/renderer/routes/sign-in/page.tsx (1)

21-24: SKIP_ENV_VALIDATION is semantically misleading when used to bypass authentication.

This variable typically controls Zod schema validation of env vars (common in t3-stack setups). Repurposing it to skip the entire sign-in flow is a naming mismatch that makes it easy to overlook the security implication — if this env var leaks into a production build, authentication is silently bypassed.

Consider using a dedicated flag (e.g., DEV_SKIP_AUTH) and guarding it behind a process.env.NODE_ENV !== 'production' check at build time.

apps/desktop/src/lib/trpc/routers/projects/projects.ts (1)

56-71: Type discrimination relies on fragile string-based property checks.

OpenNewMultiResult uses overlapping shapes (OpenNewError vs OpenNewMultiSuccess) discriminated only by the presence/absence of "multi". All UI call sites use "error" in result && !("multi" in result) which is brittle and error-prone.

A proper discriminated union with an explicit type field would make consumption safer:

♻️ Suggested approach
-type OpenNewCanceled = { canceled: true };
-type OpenNewMultiSuccess = {
-	canceled: false;
-	multi: true;
-	results: FolderOutcome[];
-};
-export type OpenNewMultiResult =
-	| OpenNewCanceled
-	| OpenNewMultiSuccess
-	| OpenNewError;
+type OpenNewCanceled = { type: "canceled" };
+type OpenNewMultiSuccess = {
+	type: "multi";
+	results: FolderOutcome[];
+};
+type OpenNewError = { type: "error"; error: string };
+export type OpenNewMultiResult =
+	| OpenNewCanceled
+	| OpenNewMultiSuccess
+	| OpenNewError;

Then UI can use switch (result.type) instead of "error" in result && !("multi" in result).

apps/desktop/src/renderer/components/NewWorkspaceModal/NewWorkspaceModal.tsx (1)

209-244: Duplicated multi-result handling logic across three components.

This exact pattern — filter successes/errors/needsGitInit, show summary toast, show per-error toasts, show git-init warnings — is repeated verbatim in StartView/index.tsx, WorkspaceSidebarFooter.tsx, and here. Consider extracting a shared helper (e.g., processOpenNewMultiResult) that takes the results and callbacks, to keep the three call sites DRY.

Also, err.selectedPath.split("/").pop() won't extract the folder name correctly on Windows (backslash separators). The getBasename helper in InitGitDialog.tsx already handles both — consider reusing it.

apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebarFooter.tsx (1)

80-93: Same cross-platform path and code duplication notes apply here.

selectedPath.split("/").pop() doesn't handle Windows backslash paths. This is the third copy of the same multi-result processing pattern (also in StartView/index.tsx and NewWorkspaceModal.tsx). A shared utility would reduce the maintenance burden.

Comment on lines +509 to +520
} catch (error) {
console.error(
"[projects/openNew] Failed to open project:",
selectedPath,
error,
);
outcomes.push({
status: "error",
selectedPath,
error: "Failed to open project",
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Error outcome discards the actual error message.

The catch block logs the real error but pushes a generic "Failed to open project" string to the user. Include the actual message so users (and support) can diagnose the issue.

🐛 Proposed fix
 			} catch (error) {
 				console.error(
 					"[projects/openNew] Failed to open project:",
 					selectedPath,
 					error,
 				);
 				outcomes.push({
 					status: "error",
 					selectedPath,
-					error: "Failed to open project",
+					error: error instanceof Error ? error.message : "Failed to open project",
 				});
 			}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch (error) {
console.error(
"[projects/openNew] Failed to open project:",
selectedPath,
error,
);
outcomes.push({
status: "error",
selectedPath,
error: "Failed to open project",
});
}
} catch (error) {
console.error(
"[projects/openNew] Failed to open project:",
selectedPath,
error,
);
outcomes.push({
status: "error",
selectedPath,
error: error instanceof Error ? error.message : "Failed to open project",
});
}
🤖 Prompt for AI Agents
In `@apps/desktop/src/lib/trpc/routers/projects/projects.ts` around lines 509 -
520, The catch block for the open-new-project flow logs the real error but
pushes a generic message into the outcomes array; update the outcomes.push call
in the catch to include the actual error text (e.g., derive message via error
instanceof Error ? error.message : String(error)) instead of the static "Failed
to open project" string so selectedPath and the real error message are returned;
locate the catch handling around the selectedPath variable in the function (the
section that logs "[projects/openNew] Failed to open project") and replace the
pushed error value accordingly.

Comment on lines +60 to 108
if ("multi" in result) {
const successes = result.results.filter(
(r) => r.status === "success",
);
const needsGitInit = result.results.filter(
(r) => r.status === "needsGitInit",
);
const errors = result.results.filter((r) => r.status === "error");

// Show summary toast for opened projects
if (successes.length > 0) {
toast.success(
successes.length === 1
? "Project opened"
: `${successes.length} projects opened`,
);

// Navigate to the first successfully opened project
navigate({
to: "/project/$projectId",
params: { projectId: successes[0].project.id },
replace: true,
});
}

// Show errors
if (errors.length > 0) {
for (const err of errors) {
toast.error(
`Failed to open ${err.selectedPath.split("/").pop()}`,
{
description: err.error,
},
);
}
}

// Prompt for git init if needed
if (needsGitInit.length > 0) {
const paths = needsGitInit.map((r) => r.selectedPath);
setInitGitDialog({
isOpen: true,
selectedPath: paths[0],
selectedPaths: paths,
});
}

if ("project" in result && result.project) {
navigate({
to: "/project/$projectId",
params: { projectId: result.project.id },
replace: true,
});
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bug: InitGitDialog is lost when both successes and needsGitInit results exist.

When successes.length > 0 and needsGitInit.length > 0, the code navigates to the first project (Line 78, which unmounts StartView) and sets the InitGitDialog state (Line 100). Since InitGitDialog is rendered inside StartView (Line 304), navigation causes it to unmount before the user can see or interact with the git-init prompt.

Consider either:

  1. Deferring navigation until after the dialog is dismissed, or
  2. Lifting InitGitDialog to a layout-level component that persists across routes.
🤖 Prompt for AI Agents
In `@apps/desktop/src/renderer/screens/main/components/StartView/index.tsx` around
lines 60 - 108, The code currently calls navigate(...) when there are successes
and also sets setInitGitDialog(...) when needsGitInit exists, causing StartView
(which renders InitGitDialog) to unmount and the dialog to be lost; fix by
deferring navigation whenever needsGitInit.length > 0: detect needsGitInit
first, call setInitGitDialog({ isOpen: true, selectedPath: ..., selectedPaths:
... }) and store the intended navigation target (e.g., in a pendingNavigate
state or include it on the dialog state), then only call navigate(...) after the
dialog is dismissed/handled; update InitGitDialog’s confirm/close handlers to
consume that pendingNavigate and perform navigate(...) (use the existing
setInitGitDialog, InitGitDialog component, and navigate function names to locate
and implement the change).

Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: feat(desktop): support multi-folder selection in Open Project dialog

Thanks for the contribution @quuu! Multi-select for project import is a solid UX improvement. I've reviewed all the changes and have feedback organized by severity.


Issues to Address

1. Duplicated result-handling logic across 3 components

The same filtering + toast pattern is copy-pasted nearly identically in StartView/index.tsx, WorkspaceSidebarFooter.tsx, and NewWorkspaceModal.tsx:

const successes = result.results.filter((r) => r.status === "success");
const needsGitInit = result.results.filter((r) => r.status === "needsGitInit");
const errors = result.results.filter((r) => r.status === "error");
// ... show toasts, handle each case

This violates DRY and makes it easy for the three call sites to drift. Consider extracting a shared helper (e.g. processMultiOpenResult(results) returning { successes, needsGitInit, errors }) and a showMultiOpenToasts(...) utility, or a custom hook like useHandleMultiOpenResult().

2. selectedPath.split("/").pop() used as a basename utility in 3 places

InitGitDialog.tsx already defines a proper getBasename() helper that handles both Unix and Windows paths. The other components use the naive split("/").pop() which won't work on Windows. Either extract getBasename to a shared util or import it.

3. Sequential processing where parallel would be better

In projects.ts, the for...of loop processes folders sequentially:

for (const selectedPath of result.filePaths) {
  // ... await getGitRoot, getDefaultBranch, etc
}

Same in WorkspaceSidebarFooter.tsx for workspace creation and InitGitDialog.tsx for git init. If a user selects 10 folders, this could be noticeably slow. Consider using Promise.allSettled for the independent operations (each folder's processing doesn't depend on the others).

4. Loss of loading feedback in WorkspaceSidebarFooter

The original code used toast.promise() which showed a loading spinner toast while the workspace was being created:

// Before
toast.promise(createBranchWorkspace.mutateAsync({ projectId: result.project.id }), {
  loading: "Opening project...",
  success: "Project opened",
  error: (err) => ...
});

The new code silently creates workspaces and only shows a success/error toast after. For multi-select with sequential processing, the user gets no visual feedback during what could be a long operation.

5. SKIP_ENV_VALIDATION used as auth bypass (scope creep)

The changes to layout.tsx and sign-in/page.tsx use SKIP_ENV_VALIDATION to bypass authentication. This flag is semantically about environment variable validation, not authentication. Using it for auth bypass is confusing and potentially risky — a dev who enables SKIP_ENV_VALIDATION to skip env checks may not realize they're also bypassing auth.

These changes are also unrelated to the multi-folder selection feature and should be in a separate PR. If an auth bypass is needed for dev, consider a dedicated SKIP_AUTH flag.

6. Unnecessary toast for single project open

The new code shows toast.success("Project opened") even when only one folder was selected — the common case. The original code didn't show a toast for single opens, which was cleaner (the navigation itself was the feedback). Consider only showing the summary toast when successes.length > 1.

This is already handled correctly in StartView but WorkspaceSidebarFooter and NewWorkspaceModal show it for single opens too. The StartView pattern is the right one:

if (successes.length > 1) {
  toast.success(`${successes.length} projects opened`);
}

Wait, actually StartView shows "Project opened" for single too. But the original WorkspaceSidebarFooter already showed "Project opened" via toast.promise, so the single-toast behavior is consistent there. I'd still recommend not toasting for single opens from StartView since it didn't before and the navigation is sufficient feedback.

7. InitGitDialog API could be cleaner

The component now has both selectedPath: string and selectedPaths?: string[] with internal normalization logic. Since this is a new feature, consider just using paths: string[] as the sole prop (callers can pass [singlePath]). The current dual-prop approach adds complexity:

const allPaths =
  selectedPaths && selectedPaths.length > 0
    ? selectedPaths
    : selectedPath
      ? [selectedPath]
      : [];

This also means selectedPath becomes a dead prop when selectedPaths is provided, which is confusing.


Minor Notes

  • The GitHub username caching in git.ts is a nice improvement. Clean implementation with TTL.
  • The "error" in result && !("multi" in result) discriminant check works but is fragile — it relies on the absence of a multi field on OpenNewError. Consider adding a type or kind discriminant field to the result types for clearer pattern matching.
  • In InitGitDialog, onClose() is called even when there are errors (errors.length > 0). This means the dialog closes and the user sees the error via onError callback. This seems intentional but worth confirming — should the dialog stay open if some folders failed?

Summary

The core feature (multi-folder selection) is well-implemented and the UX flow is thoughtful (per-folder error handling, InitGitDialog for non-git folders, summary toasts). The main concerns are:

  1. Extract shared result-handling logic to reduce duplication
  2. Separate the auth bypass changes into their own PR
  3. Consider parallel processing for better performance with many folders
  4. Use getBasename utility consistently (Windows compat)

Nice work overall — looking forward to a revised version!

Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: feat(desktop): support multi-folder selection in Open Project dialog

Thanks for the contribution @quuu! Multi-select for project import is a useful feature. I reviewed the diff carefully against our codebase conventions (AGENTS.md) and spotted a few issues — some structural, some correctness.


1. Duplicated multi-result handling across 3 call sites (Shotgun Surgery)

The filtering + toast logic for successes, needsGitInit, and errors is copy-pasted across StartView/index.tsx, WorkspaceSidebarFooter.tsx, and NewWorkspaceModal.tsx. From AGENTS.md:

| Shotgun surgery | One logical change requires edits in 5+ files | Co-locate related code; reconsider boundaries |

Consider extracting a shared helper, e.g.:

function categorizeMultiResult(results: FolderOutcome[]) {
  return {
    successes: results.filter(r => r.status === "success"),
    needsGitInit: results.filter(r => r.status === "needsGitInit"),
    errors: results.filter(r => r.status === "error"),
  };
}

Similarly, the folder name extraction via err.selectedPath.split("/").pop() is repeated in all three components — InitGitDialog already has a getBasename() utility for this. Extract and share it.


2. Race condition: navigate() + setInitGitDialog() in StartView

In StartView/index.tsx, when a multi-select contains both successful projects and folders needing git init, the handler does:

// Navigate away...
navigate({ to: "/project/$projectId", params: { projectId: successes[0].project.id }, replace: true });

// ...then try to open a dialog on the now-unmounted component
if (needsGitInit.length > 0) {
  setInitGitDialog({ isOpen: true, selectedPath: paths[0], selectedPaths: paths });
}

The navigation will unmount StartView, so the InitGitDialog will never appear. This means non-git folders are silently dropped when mixed with valid git repos. The git-init prompt should either happen before navigation, or be deferred to the target view.


3. SKIP_ENV_VALIDATION misused as auth bypass

The two "dev experience" changes repurpose SKIP_ENV_VALIDATION to skip authentication entirely:

  • sign-in/page.tsx: if (env.SKIP_ENV_VALIDATION) return <Navigate to="/workspace" replace />
  • layout.tsx: if (isPending && !env.SKIP_ENV_VALIDATION)

This is semantically misleading — SKIP_ENV_VALIDATION is for skipping environment variable validation, not for bypassing authentication. If a dev auth bypass is needed, it should use a dedicated flag (e.g., DEV_SKIP_AUTH). Mixing these concerns is confusing and potentially risky if the env var is accidentally set in production. I'd recommend dropping these two changes from this PR and handling them separately if needed.


4. UX regression in WorkspaceSidebarFooter

The original code used toast.promise() to show loading → success → error feedback during workspace creation:

toast.promise(createBranchWorkspace.mutateAsync({ projectId: result.project.id }), {
  loading: "Opening project...",
  success: "Project opened",
  error: (err) => err instanceof Error ? err.message : "Failed to open project",
});

The new code replaces this with a for...of loop that processes all projects sequentially with no loading indicator. For multi-select this makes the UI appear frozen during the loop. Consider showing some loading state (at least keeping toast.promise for single-project opens, or adding a loading toast for multi).


5. Generic error message in backend hides real errors

In projects.ts, the catch block pushes a generic message:

outcomes.push({
  status: "error",
  selectedPath,
  error: "Failed to open project",  // ← real error is only logged
});

The actual error is logged to console but never shown to the user. This makes debugging difficult. Consider passing the actual error message:

error: error instanceof Error ? error.message : "Failed to open project",

6. InitGitDialog — redundant selectedPath + selectedPaths props

The component accepts both selectedPath: string and selectedPaths?: string[], then normalizes internally. This dual-prop interface is confusing. Since you're updating all call sites anyway, consider just changing the interface to paths: string[] and keeping it clean.


7. GitHub username caching — good addition, minor note

The in-memory cache with 5-minute TTL for getGitHubUsername is a solid improvement. One minor note: caching a null result (auth failure) for 5 minutes means if a user fixes their gh auth mid-session, they'll still see failures for up to 5 minutes. Consider using a shorter TTL for error cases (e.g., 30 seconds).


Summary

The core feature (multi-select in the file picker) is welcome and the per-folder error handling approach is sound. The main areas to address:

  1. Bug: Navigate-before-dialog-open race condition in StartView (non-git folders silently lost)
  2. Scope: Drop the SKIP_ENV_VALIDATION auth bypass changes — they're unrelated and semantically wrong
  3. Code quality: Extract duplicated multi-result handling into a shared helper
  4. UX: Restore loading feedback in WorkspaceSidebarFooter for workspace creation
  5. DX: Surface real error messages from the backend instead of generic strings

Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: feat(desktop): support multi-folder selection in Open Project dialog

Thanks for the contribution @quuu! The core feature is useful — importing projects one-by-one is definitely friction. Here are my findings:


Type safety: fragile discriminated union

The OpenNewMultiResult union relies on structural "duck typing" to discriminate variants:

if ("error" in result && !("multi" in result)) { ... }

This pattern appears in all 3 UI call sites and is fragile — if someone later adds a multi field to OpenNewError, it silently breaks. A proper discriminated union would be safer:

type OpenNewMultiResult =
  | { type: "canceled" }
  | { type: "success"; results: FolderOutcome[] }
  | { type: "error"; error: string }

Or simpler: just make results always present and check result.canceled first, then result.results.


Duplicated result handling across 3 call sites

The same filtering + toast pattern is copy-pasted across StartView, WorkspaceSidebarFooter, and NewWorkspaceModal:

const successes = result.results.filter((r) => r.status === "success");
const needsGitInit = result.results.filter((r) => r.status === "needsGitInit");
const errors = result.results.filter((r) => r.status === "error");
// ... toast logic ...

Per the project's AGENTS.md "three instances" heuristic, this has hit the threshold for extraction. Consider a shared helper like:

function categorizeOutcomes(results: FolderOutcome[]) {
  return {
    successes: results.filter((r) => r.status === "success"),
    needsGitInit: results.filter((r) => r.status === "needsGitInit"),
    errors: results.filter((r) => r.status === "error"),
  };
}

InitGitDialog prop API is redundant

The component now accepts both selectedPath: string (required) and selectedPaths?: string[] (optional), then normalizes internally:

const allPaths =
  selectedPaths && selectedPaths.length > 0
    ? selectedPaths
    : selectedPath ? [selectedPath] : [];

This is confusing — selectedPath becomes dead weight when selectedPaths is provided. Cleaner to just change the prop to paths: string[] and update the one call site.


InitGitDialog now closes on partial failure (behavior change)

Original code: if any error occurred during git init, the function returned early and the dialog stayed open.

New code:

if (errors.length > 0) {
  onError(`Failed to initialize ${errors.length} folder(s): ...`);
}
onClose(); // Always called — even with errors

The dialog now closes even on partial failure. Is this intentional? The UX flow changes from "fix the problem and retry" to "dialog disappears and you see an error banner."


Single-project toast is unnecessary noise

When the user selects exactly 1 folder, they now see a "Project opened" toast. The old behavior just navigated directly. A single selection doesn't need toast confirmation — the navigation itself is the feedback. Consider suppressing the toast for successes.length === 1 cases, or at least in StartView where navigation is the primary action.


WorkspaceSidebarFooter lost its loading feedback

Old code:

toast.promise(createBranchWorkspace.mutateAsync(...), {
  loading: "Opening project...",
  success: "Project opened",
  error: (err) => ...
});

New code creates workspaces in a silent loop with no loading indicator. For multiple projects this could be noticeable.


Scope creep: unrelated changes

Two changes are unrelated to multi-folder selection and would be better as a separate PR:

  1. SKIP_ENV_VALIDATION bypass in sign-in/page.tsx and layout.tsx — modifies authentication flow
  2. getGitHubUsername caching in git.ts — adds a 5-minute TTL cache

These are fine changes on their own, but mixing auth flow modifications with a UI feature makes the PR harder to review and bisect.


Minor nits

  • selectedPath.split("/").pop() appears 4 times across the codebase for getting the folder basename. There's already a getBasename() utility in InitGitDialog.tsx — reuse it or extract it to a shared location.
  • The getGitHubUsername cache is a module-level mutable singleton. Consider that this cache never invalidates on logout/user-switch. A null value cached for 5 minutes means failed auth will be sticky.

Summary

The core feature works and the approach of collecting per-folder outcomes with independent error handling is solid. The main issues to address are:

  1. Type safety — use a proper discriminated union instead of structural checks
  2. DRY — extract the duplicated outcome handling
  3. InitGitDialog API — simplify the redundant prop interface
  4. Scope — split unrelated changes into a separate PR

Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: feat(desktop): support multi-folder selection in Open Project dialog

Thanks for contributing this @quuu! Multi-folder selection is a genuinely useful quality-of-life improvement. Here's a detailed review:


Overall Assessment

The core feature — enabling multiSelections in the native dialog and processing each folder independently — is well-conceived. The per-folder FolderOutcome type system is clean. However, there are several issues ranging from behavioral regressions to code quality concerns that should be addressed before merging.


Issues

1. InitGitDialog closes on errors (behavioral regression)

In InitGitDialog.tsx, after the loop processes all paths, onClose() is always called — even if every git init failed:

if (errors.length > 0) {
    onError(`Failed to initialize ${errors.length} folder(s): ${errors.join("; ")}`);
}
onClose(); // ← always closes, even on total failure

The old behavior would return early on error, keeping the dialog open so the user could retry. Now, if all initializations fail, the dialog dismisses itself and the user just sees an error toast. Consider only calling onClose() when at least one path succeeded, or when there are no errors.

2. InitGitDialog prop API is confusing — dual selectedPath + selectedPaths

The component now accepts both selectedPath: string (required) and selectedPaths?: string[] (optional), with internal normalization logic:

const allPaths =
    selectedPaths && selectedPaths.length > 0
        ? selectedPaths
        : selectedPath
            ? [selectedPath]
            : [];

This creates a confusing contract where callers must always pass selectedPath even when using selectedPaths. Consider simplifying to a single paths: string[] prop (breaking change is fine since this is an internal component).

3. Duplicated multi-result handling across 3 components

The same pattern — filter successes/needsGitInit/errors from result.results, then show toasts — is copy-pasted across:

  • StartView/index.tsx
  • WorkspaceSidebarFooter.tsx
  • NewWorkspaceModal.tsx

Per AGENTS.md's "three instances" heuristic, this has hit the extraction threshold. Consider a shared utility like:

function processMultiOpenResult(results: FolderOutcome[]) {
    return {
        successes: results.filter(r => r.status === "success"),
        needsGitInit: results.filter(r => r.status === "needsGitInit"),
        errors: results.filter(r => r.status === "error"),
    };
}

Or a custom hook that also handles the toast logic.

4. WorkspaceSidebarFooter lost toast.promise loading state

The original code gave users a loading indicator via toast.promise:

toast.promise(createBranchWorkspace.mutateAsync({ projectId: result.project.id }), {
    loading: "Opening project...",
    success: "Project opened",
    error: (err) => ...
});

The new code fires createBranchWorkspace.mutateAsync in a sequential loop with only error toasts. For single-project selection (the common case), the user no longer sees the "Opening project..." loading toast. This is a UX regression.

5. Sequential createBranchWorkspace calls in WorkspaceSidebarFooter

for (const s of successes) {
    try {
        await createBranchWorkspace.mutateAsync({ projectId: s.project.id });
    } catch (err) { ... }
}

This processes workspace creation sequentially. For 5+ projects, this could feel slow. Consider Promise.allSettled for parallelism (if the backend supports concurrent workspace creation).

6. Unrelated changes bundled in — SKIP_ENV_VALIDATION auth bypass

The layout.tsx and sign-in/page.tsx changes are unrelated to multi-folder selection:

// layout.tsx
if (isPending && !env.SKIP_ENV_VALIDATION) { ... }

// sign-in/page.tsx
if (env.SKIP_ENV_VALIDATION) {
    return <Navigate to="/workspace" replace />;
}

These should ideally be in a separate PR to keep the review scope focused. Additionally, SKIP_ENV_VALIDATION semantically means "skip Zod env schema validation" — using it to bypass authentication is a semantic mismatch that could confuse other developers. Consider a dedicated SKIP_AUTH env var if this is intentional.

7. Sequential git init in InitGitDialog.handleInitGit

Similar to point 5, the for (const path of allPaths) loop processes git inits sequentially. Promise.allSettled would speed up batch initialization.


Minor / Nits

  • GitHub username caching — The 5-minute in-memory cache is reasonable for dev experience. Note that it caches null (failed lookups) for 5 minutes too, so if a user runs gh auth login during a session, they'd need to wait up to 5 min. A shorter TTL for failures (e.g. 30s) might be friendlier.

  • "error" in result && !("multi" in result) check — Since OpenNewMultiSuccess doesn't have an error field, the !("multi" in result) guard is technically redundant. Not a bug, but it suggests the discriminated union could use a type discriminant field for cleaner narrowing (e.g. type: "multi" | "error" | "canceled").

  • openFromPath still returns OpenNewResult — The drag-and-drop handler in StartView (handleDrop) still uses the old single-select response format from openFromPath. This is correct since only openNew was changed, but worth noting that the two related procedures now return different shapes, which increases cognitive overhead.


What Looks Good

  • Clean FolderOutcome union type design
  • Error handling per-folder in the backend (instead of failing the entire batch)
  • Summary toasts with proper singular/plural messaging
  • The getGitHubUsername caching addresses a real dev pain point
  • Good test coverage description in the PR body

Suggested Priority

  1. Fix InitGitDialog closing on errors (regression)
  2. Extract duplicated multi-result handling
  3. Simplify InitGitDialog props
  4. Restore loading toast in WorkspaceSidebarFooter
  5. Split out SKIP_ENV_VALIDATION changes to a separate PR

Copy link
Copy Markdown
Collaborator

@Kitenite Kitenite left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: feat(desktop): support multi-folder selection in Open Project dialog

Nice feature — multi-folder selection removes real friction from the onboarding flow. The per-folder error handling approach is solid. Here are my findings:


Bug: Navigate + InitGitDialog race condition (StartView/index.tsx)

When the user selects a mix of git repos and non-git folders, the code:

  1. Navigates to the first successful project (line ~76-82)
  2. Opens InitGitDialog for the non-git folders (line ~93-100)

But navigating away from StartView unmounts it, which unmounts InitGitDialog. The dialog will flash briefly or never appear at all. The navigation should be deferred until after the InitGitDialog is dismissed, or the dialog should live higher in the component tree.


Design: Fragile type discrimination

All 3 UI call sites use this pattern:

if ("error" in result && !("multi" in result)) { ... }

This is brittle — adding a new field to any variant could break it. A proper discriminated union would be much safer:

type OpenNewMultiResult =
  | { type: "canceled" }
  | { type: "multi"; results: FolderOutcome[] }
  | { type: "error"; error: string };

Then consumers use switch (result.type) — no guessing about property existence.


Design: Duplicated multi-result handling across 3 components

The exact same pattern appears in StartView/index.tsx, WorkspaceSidebarFooter.tsx, and NewWorkspaceModal.tsx:

  • Filter successes / needsGitInit / errors
  • Show summary toast
  • Show per-error toasts
  • Show git-init warnings

This should be extracted to a shared helper (e.g., processOpenNewMultiResult(results, callbacks)) to keep things DRY. Right now each call site is ~30 lines of nearly identical logic.


Bug: selectedPath.split("/").pop() is cross-platform unsafe

Used in 3 places but doesn't handle Windows backslash paths. InitGitDialog.tsx already has a getBasename helper that handles both separators — reuse it or promote it to a shared utility.


Nit: InitGitDialog prop API is awkward

The dialog now takes both selectedPath: string and selectedPaths?: string[], with normalization logic inside:

const allPaths =
  selectedPaths && selectedPaths.length > 0
    ? selectedPaths
    : selectedPath ? [selectedPath] : [];

Simpler to just accept selectedPaths: string[] and update callers. The dual-prop API adds confusion about which one takes precedence.


Nit: Generic error message in projects.ts catch block

outcomes.push({
  status: "error",
  selectedPath,
  error: "Failed to open project",  // <-- real error is logged but not returned
});

The actual error message is logged to console but the user sees a generic string. Consider including the real error message (like the pattern used elsewhere: error instanceof Error ? error.message : String(error)).


Nit: WorkspaceSidebarFooter lost loading UX

The old code used toast.promise(createBranchWorkspace.mutateAsync(...)) which showed a "Opening project..." loading state. The new code runs createBranchWorkspace in a loop with no loading indicator per-project. For multiple projects this could feel unresponsive.


Scope: SKIP_ENV_VALIDATION changes are unrelated

The SKIP_ENV_VALIDATION bypass in layout.tsx and sign-in/page.tsx is a dev experience fix unrelated to multi-folder selection. As CodeRabbit also noted, repurposing SKIP_ENV_VALIDATION (which semantically means "skip Zod env schema validation") to bypass authentication entirely is a naming mismatch with security implications. Consider:

  1. Splitting this into a separate PR
  2. Using a dedicated flag like DEV_SKIP_AUTH with an explicit NODE_ENV !== 'production' guard

Similarly, the GitHub username caching change in git.ts is a good improvement but should be its own PR.


Summary

The core multi-select feature is well-implemented. The main items to address before merging:

  1. Fix the navigate + InitGitDialog race (bug — dialog gets unmounted)
  2. Extract shared multi-result handling (3x duplication)
  3. Use proper discriminated union types (fragile "error" in result && !("multi" in result) pattern)
  4. Split unrelated changes (SKIP_ENV_VALIDATION bypass, GitHub username caching) into separate PRs

@Kitenite
Copy link
Copy Markdown
Collaborator

Kitenite commented Feb 7, 2026

Hey sorry claude code dropped a comment on everything as my review process. im disabling that.

@Kitenite
Copy link
Copy Markdown
Collaborator

Kitenite commented Feb 7, 2026

Hey I had some changes but could not push to your branch. Will merge in a quick follow up PR

@Kitenite Kitenite merged commit 01e8d20 into superset-sh:main Feb 7, 2026
3 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants