feat: add GitHub setup step and enhance setup flow

- Introduced a new GitHubSetupStep component for GitHub CLI configuration during the setup process.
- Updated SetupView to include the GitHub step in the setup flow, allowing users to skip or proceed based on their GitHub CLI status.
- Enhanced state management to track GitHub CLI installation and authentication status.
- Added logging for transitions between setup steps to improve user feedback.
- Updated related files to ensure cross-platform path normalization and compatibility.
This commit is contained in:
Cody Seibert
2025-12-16 13:56:53 -05:00
parent 8482cdab87
commit 8c24381759
26 changed files with 1302 additions and 466 deletions

View File

@@ -34,7 +34,7 @@ import {
} from "lucide-react";
import { useAppStore } from "@/store/app-store";
import { getElectronAPI } from "@/lib/electron";
import { cn } from "@/lib/utils";
import { cn, pathsEqual, normalizePath } from "@/lib/utils";
import { toast } from "sonner";
interface WorktreeInfo {
@@ -225,10 +225,10 @@ export function WorktreeSelector({
const result = await api.worktree.startDevServer(projectPath, targetPath);
if (result.success && result.result) {
// Update running servers map
// Update running servers map (normalize path for cross-platform compatibility)
setRunningDevServers((prev) => {
const next = new Map(prev);
next.set(targetPath, {
next.set(normalizePath(targetPath), {
worktreePath: result.result!.worktreePath,
port: result.result!.port,
url: result.result!.url,
@@ -260,10 +260,10 @@ export function WorktreeSelector({
const result = await api.worktree.stopDevServer(targetPath);
if (result.success) {
// Update running servers map
// Update running servers map (normalize path for cross-platform compatibility)
setRunningDevServers((prev) => {
const next = new Map(prev);
next.delete(targetPath);
next.delete(normalizePath(targetPath));
return next;
});
toast.success(result.result?.message || "Dev server stopped");
@@ -285,8 +285,10 @@ export function WorktreeSelector({
};
// Helper to get the path key for a worktree (for looking up in runningDevServers)
// Normalizes path for cross-platform compatibility
const getWorktreeKey = (worktree: WorktreeInfo) => {
return worktree.isMain ? projectPath : worktree.path;
const path = worktree.isMain ? projectPath : worktree.path;
return path ? normalizePath(path) : path;
};
// Helper to check if a worktree has running features
@@ -301,12 +303,13 @@ export function WorktreeSelector({
if (!feature) return false;
// For main worktree, check features with no worktreePath or matching projectPath
// Use pathsEqual for cross-platform compatibility (Windows uses backslashes)
if (worktree.isMain) {
return !feature.worktreePath || feature.worktreePath === projectPath;
return !feature.worktreePath || pathsEqual(feature.worktreePath, projectPath);
}
// For other worktrees, check if worktreePath matches
return feature.worktreePath === worktreeKey;
return pathsEqual(feature.worktreePath, worktreeKey);
});
};
@@ -459,7 +462,7 @@ export function WorktreeSelector({
// currentWorktree.path is null for main, or the worktree path for others
const currentWorktreePath = currentWorktree?.path ?? null;
const selectedWorktree = currentWorktreePath
? worktrees.find((w) => w.path === currentWorktreePath)
? worktrees.find((w) => pathsEqual(w.path, currentWorktreePath))
: worktrees.find((w) => w.isMain);
@@ -469,7 +472,7 @@ export function WorktreeSelector({
// Default to main selected if currentWorktree is null/undefined or path is null
const isSelected = worktree.isMain
? currentWorktree === null || currentWorktree === undefined || currentWorktree.path === null
: worktree.path === currentWorktreePath;
: pathsEqual(worktree.path, currentWorktreePath);
const isRunning = hasRunningFeatures(worktree);

View File

@@ -47,6 +47,8 @@ export function CreatePRDialog({
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [prUrl, setPrUrl] = useState<string | null>(null);
const [browserUrl, setBrowserUrl] = useState<string | null>(null);
const [showBrowserFallback, setShowBrowserFallback] = useState(false);
// Reset state when dialog opens or worktree changes
useEffect(() => {
@@ -58,6 +60,8 @@ export function CreatePRDialog({
setIsDraft(false);
setError(null);
setPrUrl(null);
setBrowserUrl(null);
setShowBrowserFallback(false);
}
}, [open, worktree?.path]);
@@ -93,14 +97,26 @@ export function CreatePRDialog({
});
onCreated();
} else {
// Branch was pushed successfully
toast.success("Branch pushed", {
description: result.result.committed
? `Commit ${result.result.commitHash} pushed to ${result.result.branch}`
: `Branch ${result.result.branch} pushed`,
});
if (!result.result.prCreated) {
// Show the specific error if available
// Check if we should show browser fallback
if (!result.result.prCreated && result.result.browserUrl) {
const prError = result.result.prError;
// If gh CLI is not available, show browser fallback UI
if (prError === "gh_cli_not_available" || !result.result.ghCliAvailable) {
setBrowserUrl(result.result.browserUrl);
setShowBrowserFallback(true);
onCreated();
return; // Don't close dialog, show browser fallback UI
}
// gh CLI is available but failed - show error with browser option
if (prError) {
// Parse common gh CLI errors for better messages
let errorMessage = prError;
@@ -111,16 +127,25 @@ export function CreatePRDialog({
} else if (prError.includes("not logged in") || prError.includes("auth")) {
errorMessage = "GitHub CLI not authenticated. Run 'gh auth login' in terminal.";
}
// Show error but also provide browser option
setBrowserUrl(result.result.browserUrl);
setShowBrowserFallback(true);
toast.error("PR creation failed", {
description: errorMessage,
duration: 8000,
});
} else {
toast.info("PR not created", {
description: "GitHub CLI (gh) may not be installed or authenticated",
});
onCreated();
return;
}
}
// No browser URL available, just close
if (!result.result.prCreated) {
toast.info("PR not created", {
description: "GitHub CLI (gh) may not be installed or authenticated",
});
}
onCreated();
onOpenChange(false);
}
@@ -145,6 +170,8 @@ export function CreatePRDialog({
setIsDraft(false);
setError(null);
setPrUrl(null);
setBrowserUrl(null);
setShowBrowserFallback(false);
}, 200);
};
@@ -185,6 +212,32 @@ export function CreatePRDialog({
View Pull Request
</Button>
</div>
) : showBrowserFallback && browserUrl ? (
<div className="py-6 text-center space-y-4">
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-blue-500/10">
<GitPullRequest className="w-8 h-8 text-blue-500" />
</div>
<div>
<h3 className="text-lg font-semibold">Branch Pushed!</h3>
<p className="text-sm text-muted-foreground mt-1">
Your changes have been pushed to GitHub.
<br />
Click below to create a pull request in your browser.
</p>
</div>
<div className="space-y-2">
<Button
onClick={() => window.open(browserUrl, "_blank")}
className="gap-2 w-full"
>
<ExternalLink className="w-4 h-4" />
Create PR in Browser
</Button>
<p className="text-xs text-muted-foreground">
Tip: Install the GitHub CLI (<code className="bg-muted px-1 rounded">gh</code>) to create PRs directly from the app
</p>
</div>
</div>
) : (
<>
<div className="grid gap-4 py-4">

View File

@@ -1,5 +1,6 @@
import { useMemo, useCallback } from "react";
import { Feature } from "@/store/app-store";
import { pathsEqual } from "@/lib/utils";
type ColumnId = Feature["status"];
@@ -65,8 +66,8 @@ export function useBoardColumnFeatures({
// No worktree or branch assigned - show only on main
matchesWorktree = !currentWorktreePath;
} else if (f.worktreePath) {
// Has worktreePath - match by path
matchesWorktree = f.worktreePath === effectiveWorktreePath;
// Has worktreePath - match by path (use pathsEqual for cross-platform compatibility)
matchesWorktree = pathsEqual(f.worktreePath, effectiveWorktreePath);
} else if (effectiveBranch === null) {
// We're selecting a non-main worktree but can't determine its branch yet
// (worktrees haven't loaded). Don't show branch-only features until we know.