|
|
|
@@ -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()
|
|
|
|
|
{
|
|
|
|
|