Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/staged/src/lib/features/branches/BranchCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import type {
Branch,
BranchTimeline as BranchTimelineData,
ProjectRepo,
SessionStatusPayload,
WorkspaceStatus,
} from '../../types';
Expand All @@ -43,7 +44,7 @@

interface Props {
branch: Branch;
repoLabel?: { githubRepo: string; subpath: string | null; reason?: string | null } | null;
repoLabel?: ProjectRepo | null;
projectName?: string;
deleting?: boolean;
worktreeError?: string;
Expand All @@ -52,6 +53,7 @@
onRename?: (branchName: string) => void;
onRetryWorktree?: () => void;
onWorkspaceStatusChange?: (status: WorkspaceStatus, workstationId?: number | null) => void;
onDismissReason?: (projectRepoId: string) => void;
}

let {
Expand All @@ -65,6 +67,7 @@
onRename,
onRetryWorktree,
onWorkspaceStatusChange,
onDismissReason,
}: Props = $props();

// Determine if this is a local or remote branch
Expand Down Expand Up @@ -521,6 +524,7 @@
if (branch.projectRepoId) {
try {
await commands.clearProjectRepoReason(branch.projectRepoId);
onDismissReason?.(branch.projectRepoId);
} catch (e) {
console.error('Failed to clear repo reason:', e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
import SineWave from '../../shared/SineWave.svelte';
import ActionOutputModal from '../actions/ActionOutputModal.svelte';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
import type { Branch } from '../../types';
import type { Branch, ProjectRepo } from '../../types';
import * as commands from '../../api/commands';
import type { ProjectAction } from '../../api/commands';
import {
Expand Down Expand Up @@ -58,7 +58,7 @@

interface Props {
branch: Branch;
repoLabel?: { githubRepo: string; subpath: string | null; reason?: string | null } | null;
repoLabel?: ProjectRepo | null;
isLocal: boolean;
isRemote: boolean;
remoteWorkspaceStatus: string | null;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
<script lang="ts">
import { GitBranch } from 'lucide-svelte';
import RepoLabel from '../../shared/RepoLabel.svelte';
import type { ProjectRepo } from '../../types';

interface Props {
branchName: string;
repoLabel?: { githubRepo: string; subpath: string | null } | null;
repoLabel?: ProjectRepo | null;
secondaryLabel?: string | null;
}

Expand Down
146 changes: 50 additions & 96 deletions apps/staged/src/lib/features/projects/ProjectHome.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@
import { onMount } from 'svelte';
import { getCurrentWindow } from '@tauri-apps/api/window';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
import type { Project, Branch, StoreIncompatibility, WorkspaceStatus } from '../../types';
import type {
Project,
ProjectRepo,
Branch,
StoreIncompatibility,
WorkspaceStatus,
} from '../../types';
import * as commands from '../../api/commands';
import { listenToRepoActionsDetection } from '../actions/actions';
import { projectDisplayName } from '../../shared/utils';
Expand All @@ -32,9 +38,17 @@
// Data
let projects = $state<Project[]>([]);
let branchesByProject = $state<Map<string, Branch[]>>(new Map());
let repoLabelsByProject = $state<
Map<string, Map<string, { githubRepo: string; subpath: string | null; reason: string | null }>>
>(new Map());
let reposById = $state<Map<string, ProjectRepo>>(new Map());

/** Replace all cached repos for a single project with a fresh list. */
function replaceProjectRepos(projectId: string, repos: ProjectRepo[]) {
const next = new Map(reposById);
for (const [id, repo] of next) {
if (repo.projectId === projectId) next.delete(id);
}
for (const repo of repos) next.set(repo.id, repo);
reposById = next;
}
let loading = $state(true);
let error = $state<string | null>(null);
let loadGeneration = 0;
Expand Down Expand Up @@ -109,22 +123,7 @@
projects = projectsList;
branchesByProject = new Map(branchesByProject).set(projectId, branches);
workspaceLifecycle.enqueueInitialSetup(projectId, branches);
repoLabelsByProject = new Map(repoLabelsByProject).set(
projectId,
new Map(
repos.map(
(repo) =>
[
repo.id,
{
githubRepo: repo.githubRepo,
subpath: repo.subpath ?? null,
reason: repo.reason ?? null,
},
] as const
)
)
);
replaceProjectRepos(projectId, repos);
} catch (e) {
console.error('[ProjectHome] Failed to refresh project after setup progress:', e);
}
Expand Down Expand Up @@ -222,16 +221,18 @@

// Seed maps so project sections can render immediately.
const branchMap = new Map<string, Branch[]>();
const repoLabelMap = new Map<
string,
Map<string, { githubRepo: string; subpath: string | null; reason: string | null }>
>();
for (const project of projectList) {
branchMap.set(project.id, branchesByProject.get(project.id) || []);
repoLabelMap.set(project.id, repoLabelsByProject.get(project.id) || new Map());
}
branchesByProject = branchMap;
repoLabelsByProject = repoLabelMap;

// Drop cached repos for projects that no longer exist.
const projectIds = new Set(projectList.map((p) => p.id));
const prunedRepos = new Map<string, ProjectRepo>();
for (const [id, repo] of reposById) {
if (projectIds.has(repo.projectId)) prunedRepos.set(id, repo);
}
reposById = prunedRepos;

await Promise.all(
projectList.map(async (project) => {
Expand All @@ -243,22 +244,7 @@
if (generation !== loadGeneration) return;
branchesByProject = new Map(branchesByProject).set(project.id, branches);
workspaceLifecycle.enqueueInitialSetup(project.id, branches);
repoLabelsByProject = new Map(repoLabelsByProject).set(
project.id,
new Map(
repos.map(
(repo) =>
[
repo.id,
{
githubRepo: repo.githubRepo,
subpath: repo.subpath ?? null,
reason: repo.reason ?? null,
},
] as const
)
)
);
replaceProjectRepos(project.id, repos);
} catch (e) {
console.error(`[ProjectHome] Failed to hydrate project '${project.id}':`, e);
}
Expand Down Expand Up @@ -303,7 +289,10 @@
let repoCountsByProject = $derived(
new Map(
projects.map((project) => {
const knownCount = repoLabelsByProject.get(project.id)?.size ?? 0;
let knownCount = 0;
for (const repo of reposById.values()) {
if (repo.projectId === project.id) knownCount++;
}
const fallbackCount = project.githubRepo ? 1 : 0;
return [project.id, knownCount > 0 ? knownCount : fallbackCount] as const;
})
Expand Down Expand Up @@ -385,22 +374,7 @@
]);
branchesByProject = new Map(branchesByProject).set(project.id, branches);
workspaceLifecycle.enqueueInitialSetup(project.id, branches);
repoLabelsByProject = new Map(repoLabelsByProject).set(
project.id,
new Map(
repos.map(
(repo) =>
[
repo.id,
{
githubRepo: repo.githubRepo,
subpath: repo.subpath ?? null,
reason: repo.reason ?? null,
},
] as const
)
)
);
replaceProjectRepos(project.id, repos);
showNewProjectModal = false;
selectProject(project.id);
}
Expand Down Expand Up @@ -469,9 +443,11 @@
const nextBranches = new Map(branchesByProject);
nextBranches.delete(id);
branchesByProject = nextBranches;
const nextRepoLabels = new Map(repoLabelsByProject);
nextRepoLabels.delete(id);
repoLabelsByProject = nextRepoLabels;
const nextRepos = new Map(reposById);
for (const [repoId, repo] of nextRepos) {
if (repo.projectId === id) nextRepos.delete(repoId);
}
reposById = nextRepos;
for (const branch of branchesToClear) {
workspaceLifecycle.clearBranchState(branch.id);
}
Expand Down Expand Up @@ -515,22 +491,7 @@
projects = projectsList;
branchesByProject = new Map(branchesByProject).set(projectId, branches);
workspaceLifecycle.enqueueInitialSetup(projectId, branches);
repoLabelsByProject = new Map(repoLabelsByProject).set(
projectId,
new Map(
repos.map(
(repo) =>
[
repo.id,
{
githubRepo: repo.githubRepo,
subpath: repo.subpath ?? null,
reason: repo.reason ?? null,
},
] as const
)
)
);
replaceProjectRepos(projectId, repos);
} catch (e) {
console.error('Failed to add repo:', e);
const message = e instanceof Error ? e.message : String(e);
Expand Down Expand Up @@ -612,22 +573,7 @@
]);
projects = projectsList;
branchesByProject = new Map(branchesByProject).set(branch.projectId, branches);
repoLabelsByProject = new Map(repoLabelsByProject).set(
branch.projectId,
new Map(
repos.map(
(repo) =>
[
repo.id,
{
githubRepo: repo.githubRepo,
subpath: repo.subpath ?? null,
reason: repo.reason ?? null,
},
] as const
)
)
);
replaceProjectRepos(branch.projectId, repos);
} else {
await commands.deleteBranch(branch.id);
// Fallback for legacy branches without repo linkage
Expand Down Expand Up @@ -733,7 +679,7 @@
<ProjectSection
{project}
branches={branchesByProject.get(project.id) || []}
repoLabelsById={repoLabelsByProject.get(project.id) || new Map()}
{reposById}
canAddRepo={canAddRepo(project)}
addRepoHint={project.location === 'remote' ? addRepoHint(project) : null}
deleting={deletingProjectNames.has(project.id)}
Expand All @@ -749,10 +695,18 @@
onWorkspaceStatusChange={(branchId, workspaceStatus, workstationId) =>
handleWorkspaceStatusChange(project.id, branchId, workspaceStatus, workstationId)}
excludeRepos={new Set(
[...(repoLabelsByProject.get(project.id)?.values() ?? [])].map((r) => r.githubRepo)
[...reposById.values()]
.filter((r) => r.projectId === project.id)
.map((r) => r.githubRepo)
)}
onRepoSelected={(selection) => handleRepoSelected(project.id, selection)}
onRetryWorktree={(branchId) => setupBranchWorktree(branchId, project.id)}
onDismissReason={(projectRepoId) => {
const repo = reposById.get(projectRepoId);
if (repo) {
reposById = new Map(reposById).set(projectRepoId, { ...repo, reason: null });
}
}}
/>
{/each}
</div>
Expand Down
25 changes: 10 additions & 15 deletions apps/staged/src/lib/features/projects/ProjectSection.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
X,
ImagePlus,
} from 'lucide-svelte';
import type { Project, Branch, WorkspaceStatus, ProjectNote } from '../../types';
import type { Project, ProjectRepo, Branch, WorkspaceStatus, ProjectNote } from '../../types';
import { projectDisplayName } from '../../shared/utils';
import { goHome } from '../layout/navigation.svelte';
import * as commands from '../../api/commands';
Expand All @@ -45,10 +45,7 @@
interface Props {
project: Project;
branches: Branch[];
repoLabelsById?: Map<
string,
{ githubRepo: string; subpath: string | null; reason: string | null }
>;
reposById?: Map<string, ProjectRepo>;
canAddRepo?: boolean;
addRepoHint?: string | null;
deleting?: boolean;
Expand All @@ -68,12 +65,13 @@
) => void;
onRepoSelected?: (selection: RepoPickerSelection) => void;
onRetryWorktree?: (branchId: string) => void;
onDismissReason?: (projectRepoId: string) => void;
}

let {
project,
branches,
repoLabelsById = new Map(),
reposById = new Map(),
canAddRepo = true,
addRepoHint = null,
deleting = false,
Expand All @@ -89,6 +87,7 @@
onWorkspaceStatusChange,
onRepoSelected,
onRetryWorktree,
onDismissReason,
}: Props = $props();

let sortedBranches = $derived([...branches].sort((a, b) => b.createdAt - a.createdAt));
Expand Down Expand Up @@ -159,14 +158,9 @@
return () => window.removeEventListener('pointerdown', onPointerDown);
});

function repoLabelForBranch(
branch: Branch
): { githubRepo: string; subpath: string | null } | null {
const fallback = project.githubRepo
? { githubRepo: project.githubRepo, subpath: project.subpath ?? null }
: null;
if (!branch.projectRepoId) return fallback;
return repoLabelsById.get(branch.projectRepoId) ?? fallback;
function repoForBranch(branch: Branch): ProjectRepo | null {
if (!branch.projectRepoId) return null;
return reposById.get(branch.projectRepoId) ?? null;
Comment on lines +161 to +163

Choose a reason for hiding this comment

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

P2 Badge Restore primary-repo fallback for legacy branches

repoForBranch now returns null whenever branch.projectRepoId is missing, but ProjectHome.svelte still has a delete fallback for "legacy branches without repo linkage", and the backend falls back to project.primary_repo() when project_repo_id is absent. For any project created before repo-linking was added, this strips the branch card of its repo badge/reason banner and leaves DiffModal without githubRepo/subpath, even though those branches are still valid. Preserve the old project.githubRepo/project.subpath fallback for unlinked branches.

Useful? React with 👍 / 👎.

}

// ── Project session input ──────────────────────────────────────────────
Expand Down Expand Up @@ -634,7 +628,7 @@
{#each sortedBranches as branch (branch.id)}
<BranchCard
{branch}
repoLabel={repoLabelForBranch(branch)}
repoLabel={repoForBranch(branch)}
projectName={project.name}
deleting={deletingBranches.has(branch.id)}
worktreeError={worktreeErrors.get(branch.id)}
Expand All @@ -644,6 +638,7 @@
onRetryWorktree={() => onRetryWorktree?.(branch.id)}
onWorkspaceStatusChange={(status, workstationId) =>
onWorkspaceStatusChange?.(branch.id, status, workstationId)}
{onDismissReason}
/>
{/each}
</div>
Expand Down