refactor(ui): remove the stale-worker banner

The banner compared the running worker's build SHA against the
selected list's HEAD via a git ancestor check on every list switch
and reconnect. It added a git-rev-parse MSBuild target, a hub call,
and a poll loop for a corner case that hasn't come up in practice —
not worth the moving parts.
This commit is contained in:
mika kuns
2026-09-02 09:59:39 +02:00
parent b6e5876b07
commit 6be62f02cc
13 changed files with 1 additions and 221 deletions
@@ -5,7 +5,6 @@ using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data;
using ClaudeDo.Data.Git;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
@@ -52,7 +51,6 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
private readonly UpdateCheckService _updateCheck = null!;
private readonly InstallerLocator _installerLocator = null!;
private readonly WorkerLocator _workerLocator = null!;
private readonly GitService? _git;
private readonly IDbContextFactory<ClaudeDoDbContext>? _dbFactory;
private readonly Func<WorktreesOverviewModalViewModel> _worktreesOverviewVmFactory = () => null!;
private readonly Func<WeeklyReportModalViewModel> _weeklyReportVmFactory = () => null!;
@@ -121,11 +119,6 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
[ObservableProperty] private string? _updateBannerLatestVersion;
private bool _bannerDismissedThisSession;
// Persistent (not auto-clearing) banner: the running worker predates the selected list's
// merged HEAD, so "verified" claims made against the current process are stale. ClaudeDo-repo
// only — see RefreshStaleWorkerCheckAsync.
[ObservableProperty] private bool _isStaleWorkerBannerVisible;
// planningTaskId -> subtaskId, for unit-merge conflicts an MCP session (not the UI) started.
// Kept as a dictionary so a later conflict on the same planning task updates in place instead
// of piling up duplicate entries.
@@ -328,8 +321,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
Func<MergeModalViewModel> mergeVmFactory,
Func<RepoImportModalViewModel> repoImportVmFactory,
MissionControlViewModel missionControl,
UsagePillViewModel usagePill,
GitService? git = null)
UsagePillViewModel usagePill)
{
Lists = lists; Tasks = tasks; Details = details; Worker = worker;
MissionControl = missionControl;
@@ -349,9 +341,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
_usageMonitorVmFactory = usageMonitorVmFactory;
_mergeVmFactory = mergeVmFactory;
_repoImportVmFactory = repoImportVmFactory;
_git = git;
Lists.SelectionChanged += (_, _) => Tasks.LoadForList(Lists.SelectedList);
Lists.SelectionChanged += (_, _) => _ = RefreshStaleWorkerCheckAsync();
Tasks.SelectionChanged += (_, _) => Details.Bind(Tasks.SelectedTask, Tasks.SelectionSource);
Tasks.PrepRequested += () => Details.ShowPrep();
Tasks.PropertyChanged += (_, e) =>
@@ -418,7 +408,6 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
};
Worker.OperationProgressEvent += OnOperationProgress;
Worker.WorkerLogReceivedEvent += OnWorkerLogReceived;
Worker.ConnectionRestoredEvent += () => _ = RefreshStaleWorkerCheckAsync();
Worker.PlanningMergeConflictEvent += OnPlanningMergeConflict;
Worker.PlanningMergeAbortedEvent += OnPlanningMergeAborted;
Worker.PlanningCompletedEvent += OnPlanningMergeCompleted;
@@ -471,8 +460,6 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
_connectTimer.Dispose();
_primeStatusTimer.Stop();
_primeStatusTimer.Dispose();
_staleWorkerCts?.Cancel();
_staleWorkerCts?.Dispose();
}
private void RefreshBannerFromStatus()
@@ -492,57 +479,6 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
}
}
private CancellationTokenSource? _staleWorkerCts;
// Re-evaluates the stale-worker banner for the currently selected list. Cheap (one hub call +
// up to two git subprocesses) and only ever runs for a git-backed list, so it's fine to fire on
// every selection change / reconnect rather than caching.
private async Task RefreshStaleWorkerCheckAsync()
{
_staleWorkerCts?.Cancel();
var cts = new CancellationTokenSource();
_staleWorkerCts = cts;
var stale = await ComputeIsStaleWorkerAsync(Lists?.SelectedList?.WorkingDir, cts.Token);
if (cts.IsCancellationRequested) return;
IsStaleWorkerBannerVisible = stale;
}
private async Task<bool> ComputeIsStaleWorkerAsync(string? workingDir, CancellationToken ct)
{
if (_git is null || Worker is null || string.IsNullOrWhiteSpace(workingDir)) return false;
try
{
var buildInfo = await Worker.GetWorkerBuildInfoAsync();
var buildSha = buildInfo?.BuildSha;
if (string.IsNullOrWhiteSpace(buildSha)) return false;
if (!await _git.IsGitRepoAsync(workingDir, ct)) return false;
var head = await _git.RevParseHeadAsync(workingDir, ct);
var isAncestor = string.Equals(buildSha, head, StringComparison.OrdinalIgnoreCase)
? (bool?)false // equal — never "stale" on a match, and no need to ask git
: await _git.IsAncestorAsync(workingDir, buildSha, head, ct);
return ShouldShowStaleWorkerBanner(buildSha, head, isAncestor);
}
catch
{
// Worker offline, dir no longer a repo, etc. — unknown, so stay quiet.
return false;
}
}
// Pure decision extracted for testability. isAncestor is the tri-state result of
// `git merge-base --is-ancestor buildSha head`: true = worker predates head (stale), false =
// equal or diverged (never claim "stale" on a match or an unrelated history), null = unknown
// (e.g. buildSha isn't a commit this repo knows about — never treat "unknown" as "stale").
internal static bool ShouldShowStaleWorkerBanner(string? buildSha, string? headSha, bool? isAncestor)
{
if (string.IsNullOrWhiteSpace(buildSha) || string.IsNullOrWhiteSpace(headSha)) return false;
if (string.Equals(buildSha, headSha, StringComparison.OrdinalIgnoreCase)) return false;
return isAncestor == true;
}
[RelayCommand]
private void OpenMissionControl()
{