feat(history): highlight ahead/behind vs active upstream

Repo status updates for the management view are now refreshed in the
background after switching, without blocking the initial open flow.
The history graph and hover labels now distinguish local vs upstream
branches, visually marking commits that are ahead or behind.

- Add background fetch helper and shared status update logic
- Pass active upstream into HistoryPanel and render sync styling
This commit is contained in:
Christoph Brandau
2026-07-06 20:58:01 +02:00
parent 98f3902839
commit aa61199b43
3 changed files with 127 additions and 33 deletions
+38 -26
View File
@@ -395,6 +395,22 @@
void backgroundRepoStatusTick(true);
}
async function backgroundFetchRepo(path: string) {
if (!autoRefreshEnabled || !path || backgroundFetchInFlight) return;
backgroundFetchInFlight = true;
try {
await fetchRemote(path);
const nextStatus = await getStatus(path);
if (sameRepoPath(path, activeRepoPath)) applyStatus(nextStatus);
else updateRepoManagementStatus(path, nextStatus);
} catch {
// ignore; manual Fetch/Pull surfaces auth or network problems
} finally {
backgroundFetchInFlight = false;
}
}
// Keeps the repo list's branch/ahead/behind up to date for every *other* open
// tab, not just the active one, so switching to the management view (or just
// glancing at the tab bar) shows current data without an explicit fetch.
@@ -408,6 +424,25 @@
.filter((path) => !(activeView === "repository" && sameRepoPath(path, activeRepoPath)));
}
function updateRepoManagementStatus(path: string, nextStatus: GitStatus) {
const openTab = repoTabs.find((tab) => sameRepoPath(tab.path, path));
const cached = repoStatusCache[repoKey(path)];
const row: RepoTab = {
path,
name: repoNameFromPath(path),
branch: nextStatus.current_branch,
ahead: nextStatus.ahead,
behind: nextStatus.behind,
changed: nextStatus.files.length,
lastOpened: openTab?.lastOpened ?? cached?.lastOpened ?? 0,
};
if (openTab) {
repoTabs = repoTabs.map((tab) => sameRepoPath(tab.path, path) ? row : tab);
}
cacheRepoStatus(row);
}
async function backgroundRepoStatusTick(fetchFirst: boolean) {
if (!autoRefreshEnabled || backgroundRepoStatusInFlight) return;
const others = knownRepoPathsForBackground();
@@ -427,22 +462,7 @@
try {
if (fetchFirst) await fetchRemote(path);
const nextStatus = await getStatus(path);
const openTab = repoTabs.find((tab) => sameRepoPath(tab.path, path));
const cached = repoStatusCache[repoKey(path)];
const row: RepoTab = {
path,
name: repoNameFromPath(path),
branch: nextStatus.current_branch,
ahead: nextStatus.ahead,
behind: nextStatus.behind,
changed: nextStatus.files.length,
lastOpened: openTab?.lastOpened ?? cached?.lastOpened ?? 0,
};
if (openTab) {
repoTabs = repoTabs.map((tab) => sameRepoPath(tab.path, path) ? row : tab);
}
cacheRepoStatus(row);
updateRepoManagementStatus(path, nextStatus);
} catch {
// ignore this repo — same rationale as the active-repo background fetch above
}
@@ -1169,16 +1189,6 @@
// and the overlay appears to "come late".
await tick();
await new Promise<void>((resolve) => requestAnimationFrame(() => resolve()));
// Fetch first so the bundle's ahead/behind reflects the remote's current
// state instead of whatever the local remote-tracking ref last saw. Best
// effort: no credentials are passed and failures are swallowed, same as
// the periodic background fetch — auth/network issues surface via the
// manual Fetch/Pull buttons instead of blocking (or erroring) repo open.
try {
await fetchRemote(path);
} catch {
// ignore
}
// Single backend round-trip: resolves the repo and reads status, branches,
// commits and files in one pass instead of four sequential git calls.
const bundle = await openRepositoryBundle(path, 100);
@@ -1192,6 +1202,7 @@
await refreshCommitHistory(activeRepoPath, bundle.commits);
await refreshExplorerFiles(activeRepoPath, bundle.files);
lastRepoSwitchAt = Date.now();
void backgroundFetchRepo(activeRepoPath);
});
}
@@ -2817,6 +2828,7 @@
{commits}
{localBranchNames}
activeBranch={status?.current_branch ?? ""}
activeUpstream={status?.upstream ?? ""}
repositoryKey={activeRepoPath}
{hasRepository}
{isBusy}