Skip to content

Onboarding: start a project from a GitHub repo URL - #694

Merged
bborn merged 2 commits into
mainfrom
task/1-onboarding-start-a-project-from-a-github
Aug 26, 2026
Merged

Onboarding: start a project from a GitHub repo URL#694
bborn merged 2 commits into
mainfrom
task/1-onboarding-start-a-project-from-a-github

Conversation

@bborn

@bborn bborn commented Aug 25, 2026

Copy link
Copy Markdown
Owner

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/repo and relabels enter from 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, below internal/ui and reachable from both surfaces:

  • ParseRepoRef normalizes https://github.com/owner/repo (with/without .git, with/without a trailing slash), git@github.com:owner/repo.git, ssh://git@…, github.com/owner/repo, and owner/repo. Everything else is rejected with an error that names the next step — nothing reaches git clone unvalidated. An ssh-form paste clones back over ssh, so key-based auth for private repos keeps working.
  • Cloner.Resolve picks 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.Clone runs 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. CloneErrorMessage drops 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 and enter to retry, esc to cancel an in-flight clone.

Rejoining the existing path — on success the view emits repoClonedMsg{path}, which app.go hands to handleFolderPicked. Project creation isn't forked; from that point it's an ordinary local-folder project, and nothing about the repo URL is stored.

CLIty projects create <name> --repo <url>, mutually exclusive with --path (cobra flag group, plus a plain-language check in createProjectCLI); one of the two is now required. Clone progress goes to stderr so --json keeps a clean stdout.

Testing

make test and make vet pass (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 → handleFolderPicked walk, failure staying in the UI, spinner advancing, esc.

Manually smoke-tested end to end against a real repo with HOME pointed 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

  • Out of scope as specified: no GitHub issue import, no repo URL stored on the project, no clone-on-demand.
  • The clone root is ~/Projects with no override on the CLI; the TUI lets you edit the destination before cloning. A --dest flag would be a small addition if it's wanted.
  • No GUI surface for this yet — the logic lives in internal/github so the desktop app can reuse it, but it isn't wired to an HTTP route. The parity harness doesn't flag it (no new KeyMap binding), so it's a deliberate gap rather than a broken one.

🤖 Generated with Claude Code

bborn and others added 2 commits August 25, 2026 18:04
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>
@bborn

bborn commented Aug 26, 2026

Copy link
Copy Markdown
Owner Author

QA evidence — real TUI, nine screens

Driven by scripts/qa/ty-qa-clone-shots.sh (added in this PR) against an isolated ty instance, rendered with VHS. HOME points at a throwaway home for every shot, so the one clone that runs for real landed in /tmp/ty-qa/home/Projects — the real ~/Projects was verified untouched afterwards. Repo cloned: github.com/charmbracelet/bubbletea. Build: 60cd7f8.

The path a first-run user walks

1 · Welcome fork — the second option now says itself out loud.

welcome

2 · A pasted URL is recognised — the line under the list flips to enter to clone charmbracelet/bubbletea and enter is relabelled from pick to clone. The folder shelf stays put; a URL is not a filter term.

picker-paste

3 · The destination, before anything downloads — derived from the repo name, and editable. Nothing has touched the network yet.

clone-confirm

4 · Cloning — spinner, repo name, and esc to cancel. The clone runs off the update loop so the TUI keeps painting.

clone-progress

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 handleFolderPicked being reached, which is the point of the design.

clone-done

The four ways it can go sideways

Destination already holds this repo — matched through an ssh remote against an https paste. enter adopts it; nothing is downloaded.

clone-reuse

Destination is something else — steps to bubbletea-2 and says why. Still editable.

clone-collision

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, enter retries, no partial clone left behind.

clone-failed

A URL that doesn't parse — rejected inline, before git clone is ever called.

picker-error

Three things the shots caught that the unit tests couldn't

Fixed in 60cd7f8; everything above is from after.

  1. The picker went blank. Typing a URL filtered the folder list to nothing, so the panel read No folders. — useless, and untrue.
  2. The example got cut off. The inline parse error truncated at e.g. https://…, losing the part that tells you what to type.
  3. The fix got cut off. The clone failure truncated git's stderr and the hint after it, so gh auth login), or… lost the ssh half of the advice. Both wrap now, and the clone panel got 72 columns.

Screenshots published to the R2 evidence bucket via scripts/qa/ty-qa-publish.sh conventions.

@bborn
bborn merged commit eb85b42 into main Aug 26, 2026
4 checks passed
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