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
79 changes: 52 additions & 27 deletions apps/desktop/src/lib/trpc/routers/projects/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { fetchGitHubOwner, getGitHubAvatarUrl } from "./utils/github";

type Project = SelectProject;

// Return types for openNew procedure
// Return types for openNew procedure (single project)
type OpenNewCanceled = { canceled: true };
type OpenNewSuccess = { canceled: false; project: Project };
type OpenNewNeedsGitInit = {
Expand All @@ -53,6 +53,23 @@ export type OpenNewResult =
| OpenNewNeedsGitInit
| OpenNewError;

// Per-folder outcome for multi-select
export type FolderOutcome =
| { status: "success"; project: Project }
| { status: "needsGitInit"; selectedPath: string }
| { status: "error"; selectedPath: string; error: string };

// Return types for openNew procedure (multi-select)
type OpenNewMultiSuccess = {
canceled: false;
multi: true;
results: FolderOutcome[];
};
export type OpenNewMultiResult =
| OpenNewCanceled
| OpenNewMultiSuccess
| OpenNewError;

/**
* Creates or updates a project record in the database.
* If a project with the same mainRepoPath exists, updates lastOpenedAt.
Expand Down Expand Up @@ -453,49 +470,57 @@ export const createProjectsRouter = (getWindow: () => BrowserWindow | null) => {
},
),

openNew: publicProcedure.mutation(async (): Promise<OpenNewResult> => {
openNew: publicProcedure.mutation(async (): Promise<OpenNewMultiResult> => {
const window = getWindow();
if (!window) {
return { canceled: false, error: "No window available" };
}
const result = await dialog.showOpenDialog(window, {
properties: ["openDirectory"],
properties: ["openDirectory", "multiSelections"],
title: "Open Project",
});

if (result.canceled || result.filePaths.length === 0) {
return { canceled: true };
}

const selectedPath = result.filePaths[0];
const outcomes: FolderOutcome[] = [];

let mainRepoPath: string;
try {
mainRepoPath = await getGitRoot(selectedPath);
} catch (_error) {
// Return a special response so the UI can offer to initialize git
return {
canceled: false,
needsGitInit: true,
selectedPath,
};
}
for (const selectedPath of result.filePaths) {
let mainRepoPath: string;
try {
mainRepoPath = await getGitRoot(selectedPath);
} catch {
outcomes.push({ status: "needsGitInit", selectedPath });
continue;
}

const defaultBranch = await getDefaultBranch(mainRepoPath);
const project = upsertProject(mainRepoPath, defaultBranch);
try {
const defaultBranch = await getDefaultBranch(mainRepoPath);
const project = upsertProject(mainRepoPath, defaultBranch);
await ensureMainWorkspace(project);

// Auto-create main workspace if it doesn't exist
await ensureMainWorkspace(project);
track("project_opened", {
project_id: project.id,
method: "open",
});

track("project_opened", {
project_id: project.id,
method: "open",
});
outcomes.push({ status: "success", project });
} catch (error) {
console.error(
"[projects/openNew] Failed to open project:",
selectedPath,
error,
);
outcomes.push({
status: "error",
selectedPath,
error: "Failed to open project",
});
}
Comment on lines +509 to +520
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.

}

return {
canceled: false,
project,
};
return { canceled: false, multi: true, results: outcomes };
}),

openFromPath: publicProcedure
Expand Down
16 changes: 15 additions & 1 deletion apps/desktop/src/lib/trpc/routers/workspaces/utils/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,9 +312,20 @@ export async function getGitAuthorName(
}
}

let cachedGitHubUsername: { value: string | null; timestamp: number } | null =
null;
const GITHUB_USERNAME_CACHE_TTL = 5 * 60 * 1000; // 5 minutes

export async function getGitHubUsername(
_repoPath?: string,
): Promise<string | null> {
if (
cachedGitHubUsername &&
Date.now() - cachedGitHubUsername.timestamp < GITHUB_USERNAME_CACHE_TTL
) {
return cachedGitHubUsername.value;
}

const env = await getGitEnv();

try {
Expand All @@ -323,12 +334,15 @@ export async function getGitHubUsername(
["api", "user", "--jq", ".login"],
{ env, timeout: 10_000 },
);
return stdout.trim() || null;
const value = stdout.trim() || null;
cachedGitHubUsername = { value, timestamp: Date.now() };
return value;
} catch (error) {
console.warn(
"[git/getGitHubUsername] Failed to get GitHub username:",
error instanceof Error ? error.message : String(error),
);
cachedGitHubUsername = { value: null, timestamp: Date.now() };
return null;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,15 +200,49 @@ export function NewWorkspaceModal() {
try {
const result = await openNew.mutateAsync(undefined);
if (result.canceled) return;
if ("error" in result) {

if ("error" in result && !("multi" in result)) {
toast.error("Failed to open project", { description: result.error });
return;
}
if ("needsGitInit" in result) {
toast.error("Selected folder is not a git repository");
return;

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 when multiple projects imported
if (successes.length > 1) {
toast.success(`${successes.length} projects imported`);
}

// Select the first successful project
if (successes.length > 0) {
setSelectedProjectId(successes[0].project.id);
}

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

// Show git init warnings
if (needsGitInit.length > 0) {
const names = needsGitInit
.map((r) => r.selectedPath.split("/").pop())
.join(", ");
toast.error(
needsGitInit.length === 1
? "Folder is not a git repository"
: `${needsGitInit.length} folders are not git repositories`,
{ description: names },
);
}
}
setSelectedProjectId(result.project.id);
} catch (error) {
toast.error("Failed to open project", {
description:
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src/renderer/routes/_authenticated/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function AuthenticatedLayout() {
},
});

if (isPending) {
if (isPending && !env.SKIP_ENV_VALIDATION) {
if (hasLocalToken) {
return (
<div className="flex h-screen w-screen items-center justify-center bg-background">
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/renderer/routes/sign-in/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { Spinner } from "@superset/ui/spinner";
import { createFileRoute, Navigate } from "@tanstack/react-router";
import { FaGithub } from "react-icons/fa";
import { FcGoogle } from "react-icons/fc";
import { env } from "renderer/env.renderer";
import { authClient } from "renderer/lib/auth-client";
import { electronTrpc } from "renderer/lib/electron-trpc";
import { posthog } from "renderer/lib/posthog";
Expand All @@ -17,6 +18,11 @@ function SignInPage() {
const { data: session, isPending } = authClient.useSession();
const signInMutation = electronTrpc.auth.signIn.useMutation();

// Dev bypass: skip sign-in entirely
if (env.SKIP_ENV_VALIDATION) {
return <Navigate to="/workspace" replace />;
}

// Show loading while session is being fetched
if (isPending) {
return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,26 @@ const FOCUSABLE_SELECTOR =
interface InitGitDialogProps {
isOpen: boolean;
selectedPath: string;
/** Additional paths that need git init (multi-select). */
selectedPaths?: string[];
onClose: () => void;
onError: (error: string) => void;
}

export function InitGitDialog({
isOpen,
selectedPath,
selectedPaths,
onClose,
onError,
}: InitGitDialogProps) {
// Normalize: if selectedPaths provided, use that; otherwise fall back to single selectedPath
const allPaths =
selectedPaths && selectedPaths.length > 0
? selectedPaths
: selectedPath
? [selectedPath]
: [];
const utils = electronTrpc.useUtils();
const initGitAndOpen = electronTrpc.projects.initGitAndOpen.useMutation();
const createWorkspace = useCreateWorkspace();
Expand Down Expand Up @@ -109,28 +119,37 @@ export function InitGitDialog({
if (isProcessing) return; // Prevent double-clicks
setIsProcessing(true);

try {
let result: Awaited<ReturnType<typeof initGitAndOpen.mutateAsync>>;
try {
result = await initGitAndOpen.mutateAsync({ path: selectedPath });
} catch (err) {
onError(`Failed to initialize git repository: ${getErrorMessage(err)}`);
return;
}
const errors: string[] = [];

if (!result.project) {
onError("Unexpected error: project was not created");
return;
try {
for (const path of allPaths) {
let result: Awaited<ReturnType<typeof initGitAndOpen.mutateAsync>>;
try {
result = await initGitAndOpen.mutateAsync({ path });
} catch (err) {
errors.push(`${getBasename(path)}: ${getErrorMessage(err)}`);
continue;
}

if (!result.project) {
errors.push(`${getBasename(path)}: project was not created`);
continue;
}

try {
await createWorkspace.mutateAsync({ projectId: result.project.id });
} catch (err) {
errors.push(`${getBasename(path)}: ${getErrorMessage(err)}`);
}
}

// Invalidate cache in background - don't block the primary workflow
// Invalidate cache in background
utils.projects.getRecents.invalidate().catch(console.error);

try {
await createWorkspace.mutateAsync({ projectId: result.project.id });
} catch (err) {
onError(`Failed to create workspace: ${getErrorMessage(err)}`);
return;
if (errors.length > 0) {
onError(
`Failed to initialize ${errors.length} folder(s): ${errors.join("; ")}`,
);
}

onClose();
Expand All @@ -141,9 +160,9 @@ export function InitGitDialog({
}
};

if (!isOpen) return null;
if (!isOpen || allPaths.length === 0) return null;

const folderName = getBasename(selectedPath);
const isMultiple = allPaths.length > 1;

return (
// biome-ignore lint/a11y/useKeyWithClickEvents lint/a11y/noStaticElementInteractions: Modal backdrop dismiss pattern
Expand All @@ -160,32 +179,47 @@ export function InitGitDialog({
className="bg-card border border-border rounded-lg p-8 w-full max-w-md shadow-2xl"
>
<h2 id={titleId} className="text-xl font-normal text-foreground mb-4">
Initialize Git Repository
Initialize Git {isMultiple ? "Repositories" : "Repository"}
</h2>

<p className="text-sm text-muted-foreground mb-2">
The selected folder is not a git repository:
{isMultiple
? `${allPaths.length} selected folders are not git repositories:`
: "The selected folder is not a git repository:"}
</p>

<div className="bg-background border border-border rounded-md px-3 py-2 mb-6">
<span className="text-sm text-foreground font-mono">
{folderName}
</span>
<span className="text-xs text-muted-foreground block mt-1 break-all">
{selectedPath}
</span>
<div className="space-y-2 mb-6 max-h-48 overflow-y-auto">
{allPaths.map((path) => (
<div
key={path}
className="bg-background border border-border rounded-md px-3 py-2"
>
<span className="text-sm text-foreground font-mono">
{getBasename(path)}
</span>
<span className="text-xs text-muted-foreground block mt-1 break-all">
{path}
</span>
</div>
))}
</div>

<p className="text-sm text-muted-foreground mb-6">
Would you like to initialize a git repository in this folder?
Would you like to initialize{" "}
{isMultiple
? "git repositories in these folders"
: "a git repository in this folder"}
?
</p>

<div className="flex gap-3 justify-end">
<Button variant="outline" onClick={onClose} disabled={isProcessing}>
Cancel
</Button>
<Button onClick={handleInitGit} disabled={isProcessing}>
{isProcessing ? "Initializing..." : "Initialize Git"}
{isProcessing
? "Initializing..."
: `Initialize Git${isMultiple ? ` (${allPaths.length})` : ""}`}
</Button>
</div>
</div>
Expand Down
Loading
Loading