From 253943253ed1f8ee544d6debf7a85a157b867781 Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Mon, 25 May 2026 13:45:52 +1000 Subject: [PATCH] fix(staged): make Add Repo dropdown open reliably and key excludes by subpath The Add Repo dialog had two bugs. First, the click that opened the modal also reached the freshly-attached window-level click-outside listener and closed the dropdown the autofocused input had just opened. Defer the autofocus call so that initial click finishes bubbling first, and also reopen the dropdown defensively on input. Second, the project's existing repos were being excluded by `nameWithOwner` alone, which dropped the same repo at other subpaths from recents/chips and filtered GitHub search results that have no subpath at all. Key excludes by `(repo, subpath)` instead, drop the exclusion from search results entirely, and surface a duplicate error at submit time in AddRepoModal. Signed-off-by: Matt Toohey --- .../lib/features/projects/AddRepoModal.svelte | 6 ++++++ .../lib/features/projects/ProjectHome.svelte | 2 +- .../lib/features/projects/RepoConfigForm.svelte | 4 +++- .../features/projects/RepoSearchInput.svelte | 17 ++++++++++++----- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/apps/staged/src/lib/features/projects/AddRepoModal.svelte b/apps/staged/src/lib/features/projects/AddRepoModal.svelte index 7687507d..ed0e54c5 100644 --- a/apps/staged/src/lib/features/projects/AddRepoModal.svelte +++ b/apps/staged/src/lib/features/projects/AddRepoModal.svelte @@ -66,6 +66,12 @@ const normalizedBranch = branchName.trim() || undefined; const prNumber = matchedPr?.number ?? undefined; + if (excludeRepos?.has(`${selectedRepo}\x00${normalizedSubpath ?? ''}`)) { + error = 'This repo + subpath is already in the project'; + saving = false; + return; + } + onAdded({ nameWithOwner: selectedRepo, branchName: normalizedBranch, diff --git a/apps/staged/src/lib/features/projects/ProjectHome.svelte b/apps/staged/src/lib/features/projects/ProjectHome.svelte index 75e39793..35a6c968 100644 --- a/apps/staged/src/lib/features/projects/ProjectHome.svelte +++ b/apps/staged/src/lib/features/projects/ProjectHome.svelte @@ -755,7 +755,7 @@ excludeRepos={new Set( [...reposById.values()] .filter((r) => r.projectId === project.id) - .map((r) => r.githubRepo) + .map((r) => `${r.githubRepo}\x00${r.subpath ?? ''}`) )} onRepoSelected={(selection) => handleRepoSelected(project.id, selection)} onRetryWorktree={(branchId) => setupBranchWorktree(branchId, project.id)} diff --git a/apps/staged/src/lib/features/projects/RepoConfigForm.svelte b/apps/staged/src/lib/features/projects/RepoConfigForm.svelte index 437305bf..5210ea52 100644 --- a/apps/staged/src/lib/features/projects/RepoConfigForm.svelte +++ b/apps/staged/src/lib/features/projects/RepoConfigForm.svelte @@ -175,7 +175,9 @@ }); let filteredRecentRepos = $derived( - excludeRepos ? recentRepos.filter((r) => !excludeRepos.has(r.githubRepo)) : recentRepos + excludeRepos + ? recentRepos.filter((r) => !excludeRepos.has(`${r.githubRepo}\x00${r.subpath ?? ''}`)) + : recentRepos ); const dark = $derived(darkMode.value); diff --git a/apps/staged/src/lib/features/projects/RepoSearchInput.svelte b/apps/staged/src/lib/features/projects/RepoSearchInput.svelte index a39bd029..ed02c8ec 100644 --- a/apps/staged/src/lib/features/projects/RepoSearchInput.svelte +++ b/apps/staged/src/lib/features/projects/RepoSearchInput.svelte @@ -72,13 +72,13 @@ const seen = new Set(); const result: Array<{ type: 'recent' | 'repo'; data: RecentRepo | GitHubRepo }> = []; - if (directFetchRepo && !excludeRepos.has(directFetchRepo.nameWithOwner)) { + if (directFetchRepo) { result.push({ type: 'repo', data: directFetchRepo }); seen.add(directFetchRepo.nameWithOwner); } for (const r of searchResults) { - if (!seen.has(r.nameWithOwner) && !excludeRepos.has(r.nameWithOwner)) { + if (!seen.has(r.nameWithOwner)) { result.push({ type: 'repo', data: r }); seen.add(r.nameWithOwner); } @@ -94,7 +94,7 @@ : repos; for (const r of filtered) { - if (!seen.has(r.nameWithOwner) && !excludeRepos.has(r.nameWithOwner)) { + if (!seen.has(r.nameWithOwner)) { result.push({ type: 'repo', data: r }); seen.add(r.nameWithOwner); } @@ -105,7 +105,9 @@ let filteredRecentRepos = $derived.by(() => { const q = query.toLowerCase().trim(); - const base = recentRepos.filter((r) => !excludeRepos.has(r.githubRepo)); + const base = recentRepos.filter( + (r) => !excludeRepos.has(`${r.githubRepo}\x00${r.subpath ?? ''}`) + ); return q ? base.filter((r) => r.githubRepo.toLowerCase().includes(q)) : base; }); @@ -125,7 +127,11 @@ onMount(async () => { if (autofocus) { - inputEl?.focus(); + // Defer so the click that opened our parent modal finishes bubbling + // before we open the dropdown. Otherwise the trailing window-level + // handleClickOutside (registered when this component mounted) sees a + // target outside our wrapper and closes the dropdown we just opened. + setTimeout(() => inputEl?.focus(), 0); } try { recentRepos = await commands.listRecentRepos(10); @@ -148,6 +154,7 @@ } async function handleInput() { + dropdownOpen = true; const trimmed = query.trim(); const parsed = parseGitHubUrl(trimmed);