Onboarding: start a project from a GitHub repo URL - #694
Conversation
First-run onboarding could only point at a folder that already existed on disk. Now the folder picker's text input doubles as a repo field: paste a GitHub URL, press enter, and TaskYou clones it and continues down the existing folder-picked path. - internal/github/clone.go: RepoRef parsing for the forms people paste (https with/without .git or a trailing slash, git@host:owner/repo, owner/repo shorthand, host/owner/repo), plus destination resolution (reuse an existing clone of the same repo, step around an unrelated directory) and a clone that cleans up after itself on failure. Nothing is shelled out to git before parsing succeeds. - internal/ui/repoclone.go: the clone view — shows the destination before cloning, spinner while git runs, git's own stderr on failure with the destination still editable, esc to cancel. - internal/ui/folderpicker.go: recognises a pasted URL as a repo rather than a filter term, with an inline error for a URL that doesn't parse. - cmd/task: `ty projects create <name> --repo <url>`, mutually exclusive with --path, same parsing and clone. Once the clone lands the path goes to handleFolderPicked, so project creation isn't forked — from there it's an ordinary local-folder project. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
QA screenshots (scripts/qa/ty-qa-clone-shots.sh) caught three things the
unit tests can't see:
- Typing a repo URL filtered the folder list to nothing, so the picker
read "No folders." — useless, and untrue. A URL isn't a filter term:
leave the list alone and let the clone line do the talking.
- The picker's inline parse error was truncated mid-example ("e.g.
https://…"), which is the part that tells you what to type. Wrap it.
- The clone view truncated git's stderr and the auth hint after it, so
"gh auth login), or…" lost the ssh half of the fix. Wrap those too,
flag only the first line, and give the panel 72 columns — git's
messages are long.
Also adds the screenshot script itself, and the TY_QA_SHOT_ENV hook in
ty-qa-shoot.sh it needs to point HOME at a throwaway home, so a shot that
clones for real never touches the real ~/Projects.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
QA evidence — real TUI, nine screensDriven by The path a first-run user walks1 · Welcome fork — the second option now says itself out loud. 2 · A pasted URL is recognised — the line under the list flips to enter to clone charmbracelet/bubbletea and 3 · The destination, before anything downloads — derived from the repo name, and editable. Nothing has touched the network yet. 4 · Cloning — spinner, repo name, and 5 · Back on the ordinary path — the standard New Project Detected card, the same one a picked folder produces. It read the fresh checkout: instructions imported from README.md, worktree isolation on. This is The four ways it can go sidewaysDestination already holds this repo — matched through an Destination is something else — steps to Clone fails — git's own stderr, then the fix. Prompts are disabled on purpose: a git waiting on a password looks like a hung TUI. Destination stays editable, A URL that doesn't parse — rejected inline, before Three things the shots caught that the unit tests couldn'tFixed in
Screenshots published to the R2 evidence bucket via |









First-run onboarding could only point TaskYou at a folder that already existed on disk. Now you can paste a GitHub repo URL, and TaskYou clones it and continues down the exact path a picked folder takes.
Why the folder picker (not a third Welcome button)
The picker's text input already accepts typing, and a paste is the natural gesture for a URL — so the URL goes where the cursor already is. Typed text that parses as a repo URL flips the picker's count line into
⏺ enter to clone owner/repoand relabelsenterfrom pick to clone; anything else stays a filter term. A third button would have added a decision before the paste, and the folder picker would still have had to explain itself. The Welcome hint now reads "Point at a folder, or paste a GitHub URL to clone the repo first" so the option is discoverable before you get there.Text that merely looks like a URL but doesn't parse (
https://github.com/owner) gets an inline error in the same slot rather than silently filtering to nothing.What's in it
internal/github/clone.go— all the logic, belowinternal/uiand reachable from both surfaces:ParseRepoRefnormalizeshttps://github.com/owner/repo(with/without.git, with/without a trailing slash),git@github.com:owner/repo.git,ssh://git@…,github.com/owner/repo, andowner/repo. Everything else is rejected with an error that names the next step — nothing reachesgit cloneunvalidated. An ssh-form paste clones back over ssh, so key-based auth for private repos keeps working.Cloner.Resolvepicks the destination under~/Projects: an existing checkout of the same repo (by any transport) is offered for reuse instead of failing, an unrelated directory of the same name is stepped around (repo-2), an existing empty directory is used as-is.Cloner.Cloneruns git with prompts disabled (a git waiting on a password looks like a hung TUI), captures stderr, and removes whatever git managed to write when it fails or is cancelled.CloneErrorMessagedrops the progress chatter and keeps the reason, adding the fix when the failure is an auth/not-found one.internal/ui/repoclone.go— the clone view: destination shown (and editable) before cloning, a spinner with the repo name while git runs, git's own stderr on failure with the destination still editable andenterto retry,escto cancel an in-flight clone.Rejoining the existing path — on success the view emits
repoClonedMsg{path}, whichapp.gohands tohandleFolderPicked. Project creation isn't forked; from that point it's an ordinary local-folder project, and nothing about the repo URL is stored.CLI —
ty projects create <name> --repo <url>, mutually exclusive with--path(cobra flag group, plus a plain-language check increateProjectCLI); one of the two is now required. Clone progress goes to stderr so--jsonkeeps a clean stdout.Testing
make testandmake vetpass (parity included).internal/github/clone_test.go— every accepted URL form resolving to the same repo, rejected junk, clone-URL/transport round-tripping, destination collision resolution (fresh / reuse / renamed / skip-past-unrelated / empty dir), cleanup after a failed clone, non-empty destination refused, stderr distillation. The clone itself is a function seam; no test touches the network.internal/ui/repoclone_test.go,folderpicker_test.go,repoclone_flow_test.go— URL-vs-filter classification, the clone offer and its inline error, the confirm → clone →handleFolderPickedwalk, failure staying in the UI, spinner advancing, esc.Manually smoke-tested end to end against a real repo with
HOMEpointed at a scratch dir: fresh clone, reuse via the ssh form of the same repo, and a failing clone (message surfaced, nothing left on disk).Notes / follow-ups
~/Projectswith no override on the CLI; the TUI lets you edit the destination before cloning. A--destflag would be a small addition if it's wanted.internal/githubso the desktop app can reuse it, but it isn't wired to an HTTP route. The parity harness doesn't flag it (no newKeyMapbinding), so it's a deliberate gap rather than a broken one.🤖 Generated with Claude Code