feat(ui): prompt once on worker connection failure with grace timer

Adds ShowWorkerConnectionModal hook, DecideShowConnectionPrompt one-shot gate, OpenWorkerConnectionHelp relay command, and a 12 s _connectTimer to IslandsShellViewModel; covered by two new unit tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-01 12:17:01 +02:00
parent 0139607008
commit 00dc7ebccc
2 changed files with 50 additions and 0 deletions

View File

@@ -51,6 +51,9 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
// Set by MainWindow to open the global worktrees overview dialog. // Set by MainWindow to open the global worktrees overview dialog.
public Func<WorktreesOverviewModalViewModel, Task>? ShowWorktreesOverviewModal { get; set; } public Func<WorktreesOverviewModalViewModel, Task>? ShowWorktreesOverviewModal { get; set; }
// Set by MainWindow to open the worker-connection help dialog.
public Func<WorkerConnectionModalViewModel, Task>? ShowWorkerConnectionModal { get; set; }
[ObservableProperty] private bool _isUpdateBannerVisible; [ObservableProperty] private bool _isUpdateBannerVisible;
[ObservableProperty] private string? _updateBannerLatestVersion; [ObservableProperty] private string? _updateBannerLatestVersion;
[ObservableProperty] private string? _inlineUpdateStatus; [ObservableProperty] private string? _inlineUpdateStatus;
@@ -72,6 +75,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
public bool ShowLists => WindowWidth >= 780; public bool ShowLists => WindowWidth >= 780;
private readonly System.Timers.Timer _clearTimer = new(30_000) { AutoReset = false }; private readonly System.Timers.Timer _clearTimer = new(30_000) { AutoReset = false };
private readonly System.Timers.Timer _connectTimer = new(12_000) { AutoReset = false };
[ObservableProperty] private string? _primeStatus; [ObservableProperty] private string? _primeStatus;
private readonly System.Timers.Timer _primeStatusTimer = new(5_000) { AutoReset = false }; private readonly System.Timers.Timer _primeStatusTimer = new(5_000) { AutoReset = false };
@@ -220,6 +224,11 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
}; };
_primeStatusTimer.Elapsed += (_, _) => _primeStatusTimer.Elapsed += (_, _) =>
Avalonia.Threading.Dispatcher.UIThread.Post(() => PrimeStatus = null); Avalonia.Threading.Dispatcher.UIThread.Post(() => PrimeStatus = null);
_connectTimer.Elapsed += (_, _) => Dispatcher.UIThread.Post(() =>
{
if (DecideShowConnectionPrompt(IsOffline)) _ = OpenWorkerConnectionHelpAsync();
});
_connectTimer.Start();
_ = Lists.LoadAsync(); _ = Lists.LoadAsync();
_updateCheck.PropertyChanged += (_, e) => _updateCheck.PropertyChanged += (_, e) =>
{ {
@@ -269,6 +278,25 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
if (ShowAboutModal is not null) await ShowAboutModal(vm); if (ShowAboutModal is not null) await ShowAboutModal(vm);
} }
private bool _connectionPromptShown;
internal bool DecideShowConnectionPrompt(bool isOffline)
{
if (!isOffline) return false;
if (_connectionPromptShown) return false;
_connectionPromptShown = true;
return true;
}
private async Task OpenWorkerConnectionHelpAsync()
{
var vm = new WorkerConnectionModalViewModel(_workerLocator, _installerLocator);
if (ShowWorkerConnectionModal is not null) await ShowWorkerConnectionModal(vm);
}
[RelayCommand]
private Task OpenWorkerConnectionHelp() => OpenWorkerConnectionHelpAsync();
[RelayCommand] [RelayCommand]
private async Task OpenRepoImport() private async Task OpenRepoImport()
{ {

View File

@@ -0,0 +1,22 @@
using ClaudeDo.Ui.ViewModels;
using Xunit;
namespace ClaudeDo.Ui.Tests;
public class ConnectionPromptGateTests
{
[Fact]
public void Shows_once_when_offline()
{
var vm = new IslandsShellViewModel();
Assert.True(vm.DecideShowConnectionPrompt(isOffline: true));
Assert.False(vm.DecideShowConnectionPrompt(isOffline: true)); // not a second time
}
[Fact]
public void Does_not_show_when_connected_before_grace()
{
var vm = new IslandsShellViewModel();
Assert.False(vm.DecideShowConnectionPrompt(isOffline: false));
}
}