Add git clone support to Add Project modal#53
Conversation
Detect GitHub/GitLab URLs in the project name or path input and clone the repository instead of creating a blank project. Includes async modal UX with loading/success/error states and graceful path collision handling (falls back to owner-repo directory name). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Greptile SummaryThis PR adds git clone support to the Add Project modal by detecting GitHub/GitLab URLs pasted into either input tab, showing a live clone indicator, and presenting an async spinner/success/error UX while keeping the modal open. The server-side implementation uses
Confidence Score: 4/5Safe to merge after fixing the stale global error banner that persists after clone failure. One P1 finding: the global src/client/app/useKannaState.ts (P1 stale error state); src/shared/git-url.ts (SSH normalization); src/client/components/NewProjectModal.tsx (displayed path vs actual destination) Important Files Changed
Sequence DiagramsequenceDiagram
participant U as User
participant M as NewProjectModal
participant K as useKannaState
participant S as Server (ws-router)
participant FS as Filesystem
U->>M: Paste GitHub/GitLab URL
M->>M: parseGitRepoUrl() → show clone indicator
U->>M: Click "Clone"
M->>M: setCloneStatus("cloning")
M->>K: onConfirm({ mode:"clone", localPath, fallbackPath, cloneUrl })
K->>S: project.clone command
S->>FS: resolveClonePath(localPath, fallbackPath)
FS-->>S: resolved destination (primary or fallback)
S->>FS: git clone cloneUrl resolvedPath
alt Clone succeeds
FS-->>S: exit 0
S->>K: ack { projectId, localPath: resolvedPath }
K-->>M: resolves
M->>M: setCloneStatus("success")
M->>M: setTimeout → onOpenChange(false)
else Clone fails
FS-->>S: exit non-0
S->>K: error
K->>K: setCommandError(...) ⚠️ stale global banner
K-->>M: throws error
M->>M: setCloneStatus("error") + setCloneError(msg)
M->>U: Show inline error, keep modal open for retry
end
Reviews (1): Last reviewed commit: "Add git clone support to Add Project mod..." | Re-trigger Greptile |
| } catch (error) { | ||
| setCommandError(error instanceof Error ? error.message : String(error)) | ||
| // Re-throw for clone operations so the modal can show the error inline | ||
| if (intent.kind === "project_request" && intent.project.mode === "clone") { | ||
| throw error | ||
| } |
There was a problem hiding this comment.
Stale global error persists after clone retry
setCommandError is called on every clone failure, writing to the global error banner in LocalDev.tsx. Because startChatFromIntent never clears commandError at the start of a new attempt, a failed clone leaves a stale error message visible in the background even after the user successfully retries (or cancels) from the modal. For clone operations that surface errors inline, it's safer to skip the global error setter entirely — or clear it before each attempt.
| } catch (error) { | |
| setCommandError(error instanceof Error ? error.message : String(error)) | |
| // Re-throw for clone operations so the modal can show the error inline | |
| if (intent.kind === "project_request" && intent.project.mode === "clone") { | |
| throw error | |
| } | |
| } catch (error) { | |
| // For clone operations, surface the error inline in the modal only | |
| if (intent.kind === "project_request" && intent.project.mode === "clone") { | |
| throw error | |
| } | |
| setCommandError(error instanceof Error ? error.message : String(error)) |
| <p className="text-xs text-muted-foreground font-mono pl-6.5"> | ||
| {clonePath} | ||
| </p> |
There was a problem hiding this comment.
Displayed path may not match actual clone destination
The "Cloning…" spinner always shows clonePath (the primary, repo-only directory). However, if that path already exists the server falls back to owner-repo. The user would see the wrong destination during cloning and the mismatch is only discovered when the project opens. Consider showing the confirmed path from the server ack response, or explicitly noting that the displayed path is tentative.
| /** | ||
| * Normalize a git repo URL to HTTPS format for cloning. | ||
| */ | ||
| export function toCloneUrl(input: string): string { | ||
| const parsed = parseGitRepoUrl(input) | ||
| if (!parsed) return input.trim() | ||
| return `https://${parsed.host}/${parsed.owner}/${parsed.repo}.git` | ||
| } |
There was a problem hiding this comment.
SSH URLs are silently normalized to HTTPS
toCloneUrl always rewrites git@github.com:owner/repo.git to https://github.com/owner/repo.git. For private repositories that are only accessible via SSH keys (no personal access token configured), this will cause the clone to fail or prompt for credentials with no obvious explanation. The clone indicator in the UI shows the owner/repo name but not the URL that will actually be used, so the conversion is invisible to the user. Consider preserving the original SSH URL for SSH inputs, or at least noting in the clone indicator which protocol will be used.
| } | ||
| onOpenChange(false) | ||
| } | ||
| }, [canSubmit, isCloneMode, parsedGitUrl, clonePath, activeValue, tab, newPath, name, trimmedExisting, onConfirm, onOpenChange]) |
There was a problem hiding this comment.
cloneFallbackPath missing from useCallback dependency array
cloneFallbackPath is captured in the handleSubmit closure but absent from the dep array. In practice there's no stale-closure bug today (it changes only when parsedGitUrl changes, which is already tracked), but eslint-plugin-react-hooks/exhaustive-deps will flag this and a future refactor could silently break it.
| }, [canSubmit, isCloneMode, parsedGitUrl, clonePath, activeValue, tab, newPath, name, trimmedExisting, onConfirm, onOpenChange]) | |
| }, [canSubmit, isCloneMode, parsedGitUrl, clonePath, cloneFallbackPath, activeValue, tab, newPath, name, trimmedExisting, onConfirm, onOpenChange]) |
| export function isGitRepoUrl(input: string): boolean { | ||
| const trimmed = input.trim() | ||
| return GIT_URL_PATTERNS.some((pattern) => pattern.test(trimmed)) | ||
| } |
Summary
owner-repodirectory name ifrepoalready existsTest plan
https://github.com/anthropics/claude-code) into New Folder tab — should show clone indicator and clone on submitgit@gitlab.com:owner/repo.git) — should detect and show clone indicatorowner-repopath🤖 Generated with Claude Code