From 72ae93da064e2ad6715f598ef122023f6dbbbc52 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Tue, 17 Mar 2026 12:58:48 +1100 Subject: [PATCH 01/14] feat: add PR or branch picker to project creation form Add a BranchPicker component that appears under the subpath field when a repo is selected. It fetches open PRs and remote branches for the repo, then filters them as the user types. Selecting a PR uses its head ref as the branch name; selecting a branch uses its name directly. The backend create_project command now accepts an optional branch_name parameter so the chosen branch is used instead of inferring one from the project name. Changes: - New BranchPicker.svelte with search dropdown (PRs + branches) - NewProjectForm.svelte wires up branchName state and passes it through - commands.ts createProject accepts optional branchName - lib.rs create_project accepts optional branch_name, falls back to infer_branch_name when not provided --- apps/staged/src-tauri/src/lib.rs | 8 +- apps/staged/src/lib/commands.ts | 11 +- .../lib/features/projects/BranchPicker.svelte | 295 ++++++++++++++++++ .../features/projects/NewProjectForm.svelte | 19 +- 4 files changed, 329 insertions(+), 4 deletions(-) create mode 100644 apps/staged/src/lib/features/projects/BranchPicker.svelte diff --git a/apps/staged/src-tauri/src/lib.rs b/apps/staged/src-tauri/src/lib.rs index 18b8c589..fb715d4b 100644 --- a/apps/staged/src-tauri/src/lib.rs +++ b/apps/staged/src-tauri/src/lib.rs @@ -226,6 +226,7 @@ fn create_project( github_repo: Option, location: Option, subpath: Option, + branch_name: Option, ) -> Result { let store = get_store(&store)?; let trimmed = name.trim(); @@ -243,7 +244,12 @@ fn create_project( Some("remote") => store::ProjectLocation::Remote, _ => store::ProjectLocation::Local, }; - let inferred_branch_name = branches::infer_branch_name(trimmed); + let inferred_branch_name = branch_name + .as_deref() + .map(str::trim) + .filter(|s| !s.is_empty()) + .map(ToOwned::to_owned) + .unwrap_or_else(|| branches::infer_branch_name(trimmed)); let mut project = store::Project::named(trimmed); project.location = project_location; if let Some(repo) = github_repo.clone() { diff --git a/apps/staged/src/lib/commands.ts b/apps/staged/src/lib/commands.ts index 275c5590..846464ff 100644 --- a/apps/staged/src/lib/commands.ts +++ b/apps/staged/src/lib/commands.ts @@ -57,9 +57,16 @@ export function createProject( name: string, location: 'local' | 'remote', githubRepo?: string, - subpath?: string + subpath?: string, + branchName?: string ): Promise { - return invoke('create_project', { name, location, githubRepo: githubRepo ?? null, subpath }); + return invoke('create_project', { + name, + location, + githubRepo: githubRepo ?? null, + subpath, + branchName: branchName ?? null, + }); } export function deleteProject(id: string): Promise { diff --git a/apps/staged/src/lib/features/projects/BranchPicker.svelte b/apps/staged/src/lib/features/projects/BranchPicker.svelte new file mode 100644 index 00000000..6b67c2cd --- /dev/null +++ b/apps/staged/src/lib/features/projects/BranchPicker.svelte @@ -0,0 +1,295 @@ + + + +
+
+ +
+ + {#if showDropdown && filteredItems.length > 0 && !disabled} +
+ {#each filteredItems as item, i} + + {/each} +
+ {:else if showDropdown && loading && !disabled} +
+
Loading…
+
+ {/if} +
+ + diff --git a/apps/staged/src/lib/features/projects/NewProjectForm.svelte b/apps/staged/src/lib/features/projects/NewProjectForm.svelte index ca3a3d02..957ec3a9 100644 --- a/apps/staged/src/lib/features/projects/NewProjectForm.svelte +++ b/apps/staged/src/lib/features/projects/NewProjectForm.svelte @@ -17,6 +17,7 @@ import RepoSearchInput from './RepoSearchInput.svelte'; import SubpathInput from './SubpathInput.svelte'; import type { SubpathInputApi } from './SubpathInput.svelte'; + import BranchPicker from './BranchPicker.svelte'; interface Props { onCreated: (project: Project) => void; @@ -36,6 +37,8 @@ subpath = $bindable(''), }: Props = $props(); + let branchName = $state(''); + let saving = $state(false); let error = $state(null); let isMonorepo = $state(false); @@ -103,11 +106,14 @@ ? subpath.trim().replace(/^\/+|\/+$/g, '') || undefined : undefined; + const normalizedBranch = selectedRepo ? branchName.trim() || undefined : undefined; + const project = await commands.createProject( name.trim(), location, selectedRepo ?? undefined, - normalizedSubpath + normalizedSubpath, + normalizedBranch ); onCreated(project); } catch (e) { @@ -139,6 +145,8 @@ if (target.closest('.repo-search-wrapper')) return; // Don't submit if a suggestion is highlighted in the subpath dropdown if (target.closest('.subpath-input-wrapper')) return; + // Don't submit if a suggestion is highlighted in the branch picker dropdown + if (target.closest('.branch-picker-wrapper')) return; e.preventDefault(); handleCreate(); } @@ -203,6 +211,7 @@ onclick={() => { selectedRepo = null; subpath = ''; + branchName = ''; }} > @@ -248,6 +257,14 @@ bind:api={subpathApi} /> + +
+ + +
{/if} {#if error} From 31df2e4c74ac1e48ec11b3b6d15e509256fec78f Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Tue, 17 Mar 2026 13:20:36 +1100 Subject: [PATCH 02/14] fix: start worktree from remote tracking branch when it exists MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When creating a project from an existing PR, setup_worktree_sync would always create the new local branch from base_branch (e.g. origin/main), discarding all the PR's commits. The worktree ended up at the same commit as main, so the branch card showed zero commits. Now, before creating a new worktree, we check whether the remote tracking ref (origin/) exists. If it does — which is the case for branches backing open PRs — we use it as the start point so the local branch includes all the PR's commits. For genuinely new branches that don't exist on the remote yet, the behaviour is unchanged (start from base_branch). Changes: - git/worktree.rs: add remote_branch_exists() helper - git/mod.rs: export the new function - branches.rs: use origin/ as start point when available --- apps/staged/src-tauri/src/branches.rs | 14 +++++++++++++- apps/staged/src-tauri/src/git/mod.rs | 4 ++-- apps/staged/src-tauri/src/git/worktree.rs | 7 +++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/apps/staged/src-tauri/src/branches.rs b/apps/staged/src-tauri/src/branches.rs index bd4f1749..d12c0864 100644 --- a/apps/staged/src-tauri/src/branches.rs +++ b/apps/staged/src-tauri/src/branches.rs @@ -1530,10 +1530,22 @@ pub(crate) fn setup_worktree_sync(store: &Arc, branch_id: &str) -> Result ) .map_err(|e| e.to_string())? } else { + // If the branch exists on the remote (e.g. from an existing PR), + // start the new local branch from the remote tracking ref so it + // includes the PR's commits. Otherwise fall back to base_branch + // for genuinely new branches. + let remote_ref = format!("origin/{}", branch.branch_name); + let start_point = if crate::git::remote_branch_exists(&repo_path, &branch.branch_name) + .map_err(|e| e.to_string())? + { + &remote_ref + } else { + &branch.base_branch + }; match crate::git::create_worktree_at_path( &repo_path, &branch.branch_name, - &branch.base_branch, + start_point, &desired_worktree_path, ) { Ok(path) => path, diff --git a/apps/staged/src-tauri/src/git/mod.rs b/apps/staged/src-tauri/src/git/mod.rs index bc2d1076..a9f65f18 100644 --- a/apps/staged/src-tauri/src/git/mod.rs +++ b/apps/staged/src-tauri/src/git/mod.rs @@ -32,6 +32,6 @@ pub use worktree::{ create_worktree_for_existing_branch_at_path, create_worktree_from_pr, create_worktree_from_pr_at_path, get_commits_since_base, get_full_commit_log, get_head_sha, get_parent_commit, has_unpushed_commits, list_worktrees, project_worktree_path_for, - project_worktree_root_for, remove_worktree, reset_to_commit, switch_branch, - update_branch_from_pr, worktree_path_for, CommitInfo, UpdateFromPrResult, + project_worktree_root_for, remote_branch_exists, remove_worktree, reset_to_commit, + switch_branch, update_branch_from_pr, worktree_path_for, CommitInfo, UpdateFromPrResult, }; diff --git a/apps/staged/src-tauri/src/git/worktree.rs b/apps/staged/src-tauri/src/git/worktree.rs index b0fe43d6..a684a8b6 100644 --- a/apps/staged/src-tauri/src/git/worktree.rs +++ b/apps/staged/src-tauri/src/git/worktree.rs @@ -397,6 +397,13 @@ pub fn branch_exists(repo: &Path, branch_name: &str) -> Result { Ok(result.is_ok()) } +/// Check whether a remote tracking branch (`origin/`) exists. +pub fn remote_branch_exists(repo: &Path, branch_name: &str) -> Result { + let ref_name = format!("refs/remotes/origin/{branch_name}"); + let result = cli::run(repo, &["rev-parse", "--verify", &ref_name]); + Ok(result.is_ok()) +} + /// Reset HEAD to a specific commit (hard reset). /// This discards all commits after the specified commit. pub fn reset_to_commit(worktree: &Path, commit_sha: &str) -> Result<(), GitError> { From 83d9445af25e41167bd79e4d2c2f9028a59e5c8c Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Tue, 17 Mar 2026 13:47:07 +1100 Subject: [PATCH 03/14] feat: derive project name from selected PR or branch When the project name field is empty and the user selects a PR or branch in the BranchPicker, automatically populate the name: - PR selected: use the PR title directly - Branch selected: take the last path component after splitting on '/', replace '-' and '_' with spaces, and capitalize the first letter BranchPicker now exports a BranchSelection type and accepts an optional onSelect callback that fires with the selection kind, branch name, and a human-friendly label. NewProjectForm wires this up to fill the name field only when it is currently empty. --- .../lib/features/projects/BranchPicker.svelte | 14 +++++++++++- .../features/projects/NewProjectForm.svelte | 22 +++++++++++++++++-- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/apps/staged/src/lib/features/projects/BranchPicker.svelte b/apps/staged/src/lib/features/projects/BranchPicker.svelte index 6b67c2cd..b9006119 100644 --- a/apps/staged/src/lib/features/projects/BranchPicker.svelte +++ b/apps/staged/src/lib/features/projects/BranchPicker.svelte @@ -19,13 +19,21 @@ detail?: string; } + export interface BranchSelection { + kind: 'pr' | 'branch'; + branchName: string; + /** For PRs this is the PR title; for branches it is the branch name. */ + label: string; + } + interface Props { value: string; repo: string; disabled?: boolean; + onSelect?: (selection: BranchSelection) => void; } - let { value = $bindable(''), repo, disabled = false }: Props = $props(); + let { value = $bindable(''), repo, disabled = false, onSelect }: Props = $props(); let pullRequests = $state([]); let branches = $state([]); @@ -118,6 +126,10 @@ showDropdown = false; highlightedIndex = -1; inputEl?.focus(); + + // For PRs, pass the PR title (strip the "#123 " prefix); for branches, pass the branch name. + const label = item.kind === 'pr' ? item.label.replace(/^#\d+\s*/, '') : item.branchName; + onSelect?.({ kind: item.kind, branchName: item.branchName, label }); } function handleKeydown(e: KeyboardEvent) { diff --git a/apps/staged/src/lib/features/projects/NewProjectForm.svelte b/apps/staged/src/lib/features/projects/NewProjectForm.svelte index 957ec3a9..7fbf5d64 100644 --- a/apps/staged/src/lib/features/projects/NewProjectForm.svelte +++ b/apps/staged/src/lib/features/projects/NewProjectForm.svelte @@ -17,7 +17,7 @@ import RepoSearchInput from './RepoSearchInput.svelte'; import SubpathInput from './SubpathInput.svelte'; import type { SubpathInputApi } from './SubpathInput.svelte'; - import BranchPicker from './BranchPicker.svelte'; + import BranchPicker, { type BranchSelection } from './BranchPicker.svelte'; interface Props { onCreated: (project: Project) => void; @@ -152,6 +152,19 @@ } } + /** Derive a human-friendly project name from a branch name like "feat/dark-mode". */ + function nameFromBranch(branch: string): string { + const last = branch.split('/').pop() ?? branch; + const spaced = last.replace(/[-_]/g, ' '); + return spaced.charAt(0).toUpperCase() + spaced.slice(1); + } + + function handleBranchSelected(selection: BranchSelection) { + if (!name.trim()) { + name = selection.kind === 'pr' ? selection.label : nameFromBranch(selection.branchName); + } + } + function handleRepoSelected(nameWithOwner: string, selectedSubpath?: string) { selectedRepo = nameWithOwner; subpath = selectedSubpath ?? ''; @@ -263,7 +276,12 @@ >PR or Branch Optional - + {/if} From 0152f74b4b0c896000c80c03c4172b857c50a34a Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Tue, 17 Mar 2026 13:51:46 +1100 Subject: [PATCH 04/14] feat: show 'New branch' badge when typed branch has no remote match When the user types into the PR or Branch field and the value doesn't match any known remote branch or PR head ref, the badge on the field label switches from 'Optional' to 'New branch' (with accent styling). BranchPicker now exposes a bindable isNewBranch prop that is true when the trimmed input is non-empty and not found in the set of known remote branch names. NewProjectForm binds to this prop and conditionally renders the badge text and CSS class. --- .../lib/features/projects/BranchPicker.svelte | 27 ++++++++++++++++++- .../features/projects/NewProjectForm.svelte | 11 +++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/apps/staged/src/lib/features/projects/BranchPicker.svelte b/apps/staged/src/lib/features/projects/BranchPicker.svelte index b9006119..6e694acd 100644 --- a/apps/staged/src/lib/features/projects/BranchPicker.svelte +++ b/apps/staged/src/lib/features/projects/BranchPicker.svelte @@ -30,10 +30,17 @@ value: string; repo: string; disabled?: boolean; + isNewBranch?: boolean; onSelect?: (selection: BranchSelection) => void; } - let { value = $bindable(''), repo, disabled = false, onSelect }: Props = $props(); + let { + value = $bindable(''), + repo, + disabled = false, + isNewBranch = $bindable(false), + onSelect, + }: Props = $props(); let pullRequests = $state([]); let branches = $state([]); @@ -66,6 +73,24 @@ } } + /** All known remote branch names (without origin/ prefix). */ + let knownBranches = $derived.by((): Set => { + const names = new Set(); + for (const pr of pullRequests) { + names.add(pr.headRef); + } + for (const ref of branches) { + names.add(ref.name.replace(/^origin\//, '')); + } + return names; + }); + + // Update isNewBranch whenever value or known branches change + $effect(() => { + const trimmed = value.trim(); + isNewBranch = trimmed.length > 0 && !knownBranches.has(trimmed); + }); + /** Build a unified list of picker items from PRs and branches. */ let allItems = $derived.by((): PickerItem[] => { const items: PickerItem[] = []; diff --git a/apps/staged/src/lib/features/projects/NewProjectForm.svelte b/apps/staged/src/lib/features/projects/NewProjectForm.svelte index 7fbf5d64..73a368aa 100644 --- a/apps/staged/src/lib/features/projects/NewProjectForm.svelte +++ b/apps/staged/src/lib/features/projects/NewProjectForm.svelte @@ -38,6 +38,7 @@ }: Props = $props(); let branchName = $state(''); + let isNewBranch = $state(false); let saving = $state(false); let error = $state(null); @@ -274,10 +275,13 @@
{isNewBranch ? 'New branch' : 'Optional'} Date: Tue, 17 Mar 2026 13:58:07 +1100 Subject: [PATCH 05/14] feat: show matched PR info in branch picker and save PR on project creation When the user selects a PR from the BranchPicker dropdown, display a compact info card below the input showing the PR number, title, author, base ref, and draft status. Typing into the field clears the card so stale PR info doesn't linger when the user switches to a different branch. The matched PR number is threaded through the project creation flow: - BranchPicker exposes a bindable matchedPr prop - NewProjectForm binds to it and passes the PR number to createProject - commands.ts forwards the optional prNumber parameter - The backend create_project command accepts pr_number and calls .with_pr() on the branch record for both local and remote projects This ensures that when a project is created from an existing PR, the branch's pr_number is set in the database from the start, enabling PR status features (checks, review decision, mergeable) to work without requiring the user to manually link the PR afterwards. --- apps/staged/src-tauri/src/lib.rs | 12 +++- apps/staged/src/lib/commands.ts | 4 +- .../lib/features/projects/BranchPicker.svelte | 61 +++++++++++++++++++ .../features/projects/NewProjectForm.svelte | 8 ++- 4 files changed, 81 insertions(+), 4 deletions(-) diff --git a/apps/staged/src-tauri/src/lib.rs b/apps/staged/src-tauri/src/lib.rs index fb715d4b..fb9f511a 100644 --- a/apps/staged/src-tauri/src/lib.rs +++ b/apps/staged/src-tauri/src/lib.rs @@ -218,6 +218,7 @@ fn list_projects( .map_err(|e| e.to_string()) } +#[allow(clippy::too_many_arguments)] #[tauri::command(rename_all = "camelCase")] fn create_project( store: tauri::State<'_, Mutex>>>, @@ -227,6 +228,7 @@ fn create_project( location: Option, subpath: Option, branch_name: Option, + pr_number: Option, ) -> Result { let store = get_store(&store)?; let trimmed = name.trim(); @@ -298,21 +300,27 @@ fn create_project( let branch_id = match project.location { store::ProjectLocation::Local => { - let branch = + let mut branch = store::Branch::new(&project.id, &inferred_branch_name, &effective_base) .with_project_repo(&project_repo.id); + if let Some(pr) = pr_number { + branch = branch.with_pr(pr); + } store.create_branch(&branch).map_err(|e| e.to_string())?; Some(branch.id) } store::ProjectLocation::Remote => { let workspace_name = branches::infer_workspace_name(&inferred_branch_name); - let branch = store::Branch::new_remote( + let mut branch = store::Branch::new_remote( &project.id, &inferred_branch_name, &effective_base, &workspace_name, ) .with_project_repo(&project_repo.id); + if let Some(pr) = pr_number { + branch = branch.with_pr(pr); + } store.create_branch(&branch).map_err(|e| e.to_string())?; log::info!( "[create_project] created remote branch={} workspace={} status=starting project={}", diff --git a/apps/staged/src/lib/commands.ts b/apps/staged/src/lib/commands.ts index 846464ff..1d737c1c 100644 --- a/apps/staged/src/lib/commands.ts +++ b/apps/staged/src/lib/commands.ts @@ -58,7 +58,8 @@ export function createProject( location: 'local' | 'remote', githubRepo?: string, subpath?: string, - branchName?: string + branchName?: string, + prNumber?: number ): Promise { return invoke('create_project', { name, @@ -66,6 +67,7 @@ export function createProject( githubRepo: githubRepo ?? null, subpath, branchName: branchName ?? null, + prNumber: prNumber ?? null, }); } diff --git a/apps/staged/src/lib/features/projects/BranchPicker.svelte b/apps/staged/src/lib/features/projects/BranchPicker.svelte index 6e694acd..773bba3c 100644 --- a/apps/staged/src/lib/features/projects/BranchPicker.svelte +++ b/apps/staged/src/lib/features/projects/BranchPicker.svelte @@ -31,6 +31,8 @@ repo: string; disabled?: boolean; isNewBranch?: boolean; + /** The PR that matches the current branch value, if any. */ + matchedPr?: PullRequest | null; onSelect?: (selection: BranchSelection) => void; } @@ -39,6 +41,7 @@ repo, disabled = false, isNewBranch = $bindable(false), + matchedPr = $bindable(null), onSelect, }: Props = $props(); @@ -134,6 +137,8 @@ function handleInput() { highlightedIndex = -1; showDropdown = true; + // User is typing freely — clear any previously matched PR + matchedPr = null; } function handleFocus() { @@ -152,6 +157,13 @@ highlightedIndex = -1; inputEl?.focus(); + // Update matched PR: set when selecting a PR item, clear for branches + if (item.kind === 'pr') { + matchedPr = pullRequests.find((pr) => pr.headRef === item.branchName) ?? null; + } else { + matchedPr = null; + } + // For PRs, pass the PR title (strip the "#123 " prefix); for branches, pass the branch name. const label = item.kind === 'pr' ? item.label.replace(/^#\d+\s*/, '') : item.branchName; onSelect?.({ kind: item.kind, branchName: item.branchName, label }); @@ -229,6 +241,19 @@
Loading…
{/if} + + {#if matchedPr} +
+ +
+ #{matchedPr.number} {matchedPr.title} + {matchedPr.author} · {matchedPr.baseRef}{#if matchedPr.draft} + · Draft{/if} +
+
+ {/if} diff --git a/apps/staged/src/lib/features/projects/NewProjectForm.svelte b/apps/staged/src/lib/features/projects/NewProjectForm.svelte index 73a368aa..93d68439 100644 --- a/apps/staged/src/lib/features/projects/NewProjectForm.svelte +++ b/apps/staged/src/lib/features/projects/NewProjectForm.svelte @@ -18,6 +18,7 @@ import SubpathInput from './SubpathInput.svelte'; import type { SubpathInputApi } from './SubpathInput.svelte'; import BranchPicker, { type BranchSelection } from './BranchPicker.svelte'; + import type { PullRequest } from '../../types'; interface Props { onCreated: (project: Project) => void; @@ -39,6 +40,7 @@ let branchName = $state(''); let isNewBranch = $state(false); + let matchedPr = $state(null); let saving = $state(false); let error = $state(null); @@ -108,13 +110,15 @@ : undefined; const normalizedBranch = selectedRepo ? branchName.trim() || undefined : undefined; + const prNumber = matchedPr?.number ?? undefined; const project = await commands.createProject( name.trim(), location, selectedRepo ?? undefined, normalizedSubpath, - normalizedBranch + normalizedBranch, + prNumber ); onCreated(project); } catch (e) { @@ -226,6 +230,7 @@ selectedRepo = null; subpath = ''; branchName = ''; + matchedPr = null; }} > @@ -282,6 +287,7 @@ Date: Tue, 17 Mar 2026 14:13:32 +1100 Subject: [PATCH 06/14] feat: extract PR and branch info from pasted GitHub URLs When a GitHub PR URL (e.g. github.com/owner/repo/pull/123) or branch URL (e.g. github.com/owner/repo/tree/feat/my-feature) is pasted into the repository search field, the repo is selected and the PR or branch is automatically populated in the BranchPicker. For PR URLs, the PR number is matched against fetched PRs to show the full PR info card. For branch URLs, the branch name is matched against remote branches (or PRs backed by that branch); if no match is found it appears as a new branch. Changes: - RepoSearchInput: parseGitHubUrl now returns a RepoSelection object with optional prNumber and branchName fields. onSelect callback updated to pass the full selection. - BranchPicker: new initialPrNumber and initialBranchName bindable props that auto-select after data loads, then clear themselves. - NewProjectForm: threads pending PR/branch state from RepoSearchInput through to BranchPicker via the new props. --- .../lib/features/projects/BranchPicker.svelte | 49 ++++++++++++++++++ .../features/projects/NewProjectForm.svelte | 27 ++++++++-- .../features/projects/RepoSearchInput.svelte | 50 ++++++++++++++++--- 3 files changed, 113 insertions(+), 13 deletions(-) diff --git a/apps/staged/src/lib/features/projects/BranchPicker.svelte b/apps/staged/src/lib/features/projects/BranchPicker.svelte index 773bba3c..b08aeabc 100644 --- a/apps/staged/src/lib/features/projects/BranchPicker.svelte +++ b/apps/staged/src/lib/features/projects/BranchPicker.svelte @@ -33,6 +33,10 @@ isNewBranch?: boolean; /** The PR that matches the current branch value, if any. */ matchedPr?: PullRequest | null; + /** When set, auto-select this PR after data loads. Cleared after use. */ + initialPrNumber?: number | null; + /** When set, auto-fill this branch name after data loads. Cleared after use. */ + initialBranchName?: string | null; onSelect?: (selection: BranchSelection) => void; } @@ -42,6 +46,8 @@ disabled = false, isNewBranch = $bindable(false), matchedPr = $bindable(null), + initialPrNumber = $bindable(null), + initialBranchName = $bindable(null), onSelect, }: Props = $props(); @@ -71,6 +77,49 @@ ]); pullRequests = prs; branches = refs; + + // Auto-select a PR if initialPrNumber was provided (e.g. from a pasted PR URL) + if (initialPrNumber != null) { + const pr = prs.find((p) => p.number === initialPrNumber); + if (pr) { + selectItem({ + kind: 'pr', + label: `#${pr.number} ${pr.title}`, + branchName: pr.headRef, + detail: pr.headRef, + }); + } + initialPrNumber = null; + } + // Auto-fill a branch name if initialBranchName was provided (e.g. from a pasted branch URL) + else if (initialBranchName) { + const branchNameToFind = initialBranchName; + // Check PRs first — a branch might back a PR + const prForBranch = prs.find((p) => p.headRef === branchNameToFind); + if (prForBranch) { + selectItem({ + kind: 'pr', + label: `#${prForBranch.number} ${prForBranch.title}`, + branchName: prForBranch.headRef, + detail: prForBranch.headRef, + }); + } else { + // Check remote branches + const refName = refs.find((r) => r.name.replace(/^origin\//, '') === branchNameToFind); + if (refName) { + selectItem({ + kind: 'branch', + label: branchNameToFind, + branchName: branchNameToFind, + }); + } else { + // Branch not in the list — just set the value (will show as "New branch") + value = branchNameToFind; + onSelect?.({ kind: 'branch', branchName: branchNameToFind, label: branchNameToFind }); + } + } + initialBranchName = null; + } } finally { loading = false; } diff --git a/apps/staged/src/lib/features/projects/NewProjectForm.svelte b/apps/staged/src/lib/features/projects/NewProjectForm.svelte index 93d68439..cfc1cd2c 100644 --- a/apps/staged/src/lib/features/projects/NewProjectForm.svelte +++ b/apps/staged/src/lib/features/projects/NewProjectForm.svelte @@ -18,6 +18,7 @@ import SubpathInput from './SubpathInput.svelte'; import type { SubpathInputApi } from './SubpathInput.svelte'; import BranchPicker, { type BranchSelection } from './BranchPicker.svelte'; + import type { RepoSelection } from './RepoSearchInput.svelte'; import type { PullRequest } from '../../types'; interface Props { @@ -140,7 +141,10 @@ if (idx < recentRepos.length) { e.preventDefault(); const recent = recentRepos[idx]; - handleRepoSelected(recent.githubRepo, recent.subpath ?? undefined); + handleRepoSelected({ + nameWithOwner: recent.githubRepo, + subpath: recent.subpath ?? undefined, + }); } return; } @@ -170,9 +174,14 @@ } } - function handleRepoSelected(nameWithOwner: string, selectedSubpath?: string) { - selectedRepo = nameWithOwner; - subpath = selectedSubpath ?? ''; + let pendingPrNumber = $state(null); + let pendingBranchName = $state(null); + + function handleRepoSelected(selection: RepoSelection) { + selectedRepo = selection.nameWithOwner; + subpath = selection.subpath ?? ''; + pendingPrNumber = selection.prNumber ?? null; + pendingBranchName = selection.branchName ?? null; } @@ -231,6 +240,8 @@ subpath = ''; branchName = ''; matchedPr = null; + pendingPrNumber = null; + pendingBranchName = null; }} > @@ -245,7 +256,11 @@ {#each recentRepos.slice(0, 5) as recent, i}