Skip to content

Add git clone support to Add Project modal#53

Open
anglinb wants to merge 1 commit intojakemor:mainfrom
anglinb:feat/clone-from-url
Open

Add git clone support to Add Project modal#53
anglinb wants to merge 1 commit intojakemor:mainfrom
anglinb:feat/clone-from-url

Conversation

@anglinb
Copy link
Copy Markdown
Contributor

@anglinb anglinb commented Apr 17, 2026

Summary

  • Detect GitHub/GitLab URLs (HTTPS and SSH) pasted into the Add Project modal and clone the repo instead of creating a blank project
  • Show inline clone indicator with repo owner/name and destination path when a URL is detected
  • Async modal UX: spinner during clone, green checkmark on success, inline error on failure with retry
  • Graceful path collision handling: falls back to owner-repo directory name if repo already exists

Test plan

  • Paste a GitHub HTTPS URL (e.g. https://github.com/anthropics/claude-code) into New Folder tab — should show clone indicator and clone on submit
  • Paste a GitLab SSH URL (e.g. git@gitlab.com:owner/repo.git) — should detect and show clone indicator
  • Clone a repo where the directory name already exists — should fall back to owner-repo path
  • Clone a nonexistent repo — should show inline error, modal stays open for retry
  • Verify regular project creation (no URL) still works as before
  • Verify Existing Path tab also detects URLs and triggers clone mode

🤖 Generated with Claude Code

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-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 17, 2026

Greptile Summary

This 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 spawn safely (no shell injection risk) and handles path collisions gracefully.

  • P1 (useKannaState.ts): On clone failure, both setCommandError (global banner) and re-throw (inline modal error) fire. The global banner is never cleared on successful retry, so a stale error message remains visible in the background after the user recovers from the modal.
  • P2 (NewProjectModal.tsx): The "Cloning…" view hard-codes clonePath (the primary directory) even though the server may silently use the owner-repo fallback — the displayed path can mismatch the actual destination.
  • P2 (git-url.ts): SSH URLs are normalized to HTTPS without any UI indication; private repos requiring SSH-key auth will silently fail or prompt for credentials.

Confidence Score: 4/5

Safe to merge after fixing the stale global error banner that persists after clone failure.

One P1 finding: the global commandError is set on clone failure but never cleared on subsequent success, leaving a stale error visible after the user recovers. The remaining findings are P2 (UX inconsistency in displayed path, SSH→HTTPS silent conversion, unused export, missing dep). The core clone flow — URL detection, path collision handling, async modal UX, server-side spawn — is well-implemented.

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

Filename Overview
src/client/app/useKannaState.ts Clone mode re-throws after setting global commandError, causing a stale error to persist in the background UI even after successful retry or cancel.
src/client/components/NewProjectModal.tsx Async clone UX with spinner/success/error states; clonePath shown during cloning may not match the actual server-selected destination when fallback is used; cloneFallbackPath missing from useCallback deps.
src/shared/git-url.ts New utility for parsing GitHub/GitLab URLs; toCloneUrl silently converts SSH URLs to HTTPS which can break private-repo SSH workflows; isGitRepoUrl is exported but unused.
src/server/paths.ts Adds resolveClonePath (collision-safe destination picker) and cloneRepository (safe spawn-based git clone); implementation is correct and injection-safe.
src/server/ws-router.ts Adds project.clone handler; correctly resolves path, clones, opens project, and returns actual localPath to client.
src/shared/protocol.ts Adds project.clone command type to the client command union; straightforward and consistent with existing commands.
src/client/components/LocalDev.tsx Props updated to accept clone mode; onConfirm now returns the Promise directly instead of void onCreateProject(project), enabling proper error propagation to the modal.

Sequence Diagram

sequenceDiagram
    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
Loading

Fix All in Codex

Reviews (1): Last reviewed commit: "Add git clone support to Add Project mod..." | Re-trigger Greptile

Comment on lines 1156 to +1161
} 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
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 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.

Suggested change
} 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))

Fix in Codex

Comment on lines +172 to +174
<p className="text-xs text-muted-foreground font-mono pl-6.5">
{clonePath}
</p>
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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.

Fix in Codex

Comment thread src/shared/git-url.ts
Comment on lines +47 to +54
/**
* 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`
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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.

Fix in Codex

}
onOpenChange(false)
}
}, [canSubmit, isCloneMode, parsedGitUrl, clonePath, activeValue, tab, newPath, name, trimmedExisting, onConfirm, onOpenChange])
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 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.

Suggested change
}, [canSubmit, isCloneMode, parsedGitUrl, clonePath, activeValue, tab, newPath, name, trimmedExisting, onConfirm, onOpenChange])
}, [canSubmit, isCloneMode, parsedGitUrl, clonePath, cloneFallbackPath, activeValue, tab, newPath, name, trimmedExisting, onConfirm, onOpenChange])

Fix in Codex

Comment thread src/shared/git-url.ts
Comment on lines +22 to +25
export function isGitRepoUrl(input: string): boolean {
const trimmed = input.trim()
return GIT_URL_PATTERNS.some((pattern) => pattern.test(trimmed))
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 isGitRepoUrl is exported but unused

isGitRepoUrl is never imported anywhere in the codebase — all callers use parseGitRepoUrl directly and check for null. Either remove it or convert it to an internal helper to keep the public API surface lean.

Fix in Codex

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.

1 participant