diff --git a/src/ClaudeDo.Ui/Services/RepoLinkage.cs b/src/ClaudeDo.Ui/Services/RepoLinkage.cs new file mode 100644 index 00000000..acc3dbf4 --- /dev/null +++ b/src/ClaudeDo.Ui/Services/RepoLinkage.cs @@ -0,0 +1,19 @@ +using System; +using System.Linq.Expressions; +using ClaudeDo.Data.Models; + +namespace ClaudeDo.Ui.Services; + +// Single definition of "does this list have a linked repo" — a WorkingDir that is set to +// something other than blank/whitespace. ListsIslandViewModel and SettingsModalViewModel each +// answer this over a different scope (UserLists in memory vs. all Lists rows in the DB) and need +// it in different shapes, but both must agree on what "linked" means. +public static class RepoLinkage +{ + public static bool IsLinked(string? workingDir) => !string.IsNullOrWhiteSpace(workingDir); + + // EF Core can't translate string.IsNullOrWhiteSpace against Sqlite, so the DB-side check is + // spelled out via Trim() instead — same semantics as IsLinked above. + public static readonly Expression> IsLinkedInDb = + l => l.WorkingDir != null && l.WorkingDir.Trim() != ""; +} diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs index ebeba0de..7e990795 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs @@ -187,12 +187,14 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable [ObservableProperty] private ListNavItemViewModel? _selectedList; /// True whenever no User list has a linked WorkingDir — drives the no-repo - /// banner. Smart/Virtual lists never count. Recomputed after every load and every - /// list CRUD / list-settings save; never persisted, no dismiss state. + /// banner. Smart/Virtual lists never count (scope intentionally narrower than + /// SettingsModalViewModel.HasLinkedRepo, which checks all Lists rows in the DB). Recomputed + /// after every load and every list CRUD / list-settings save; never persisted, no dismiss + /// state. [ObservableProperty] private bool _hasNoLinkedRepo = true; private void RecomputeHasNoLinkedRepo() => - HasNoLinkedRepo = UserLists.All(r => string.IsNullOrWhiteSpace(r.WorkingDir)); + HasNoLinkedRepo = UserLists.All(r => !RepoLinkage.IsLinked(r.WorkingDir)); public string UserName { get; } = Environment.UserName; public string MachineName { get; } = Environment.MachineName; diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs index d65cf16c..3efba36c 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs @@ -56,6 +56,8 @@ public sealed partial class SettingsModalViewModel : ViewModelBase // True once at least one list has a linked repo (WorkingDir). Drives the repo-hint strip on // Worktrees/Prime Claude/Session Skills/Berichte — nothing is ever hidden, just annotated. + // Scope intentionally broader than ListsIslandViewModel.HasNoLinkedRepo (all Lists rows in + // the DB, not just UserLists). [ObservableProperty] private bool _hasLinkedRepo; // Wired by WindowDialogService to close this modal and open the existing repo-import flow. @@ -152,7 +154,7 @@ public sealed partial class SettingsModalViewModel : ViewModelBase General.LoadModelPresets(dto?.ModelPresets, General.MaxTurnsCeiling); await using var ctx = await _dbFactory.CreateDbContextAsync(); - HasLinkedRepo = await ctx.Lists.AnyAsync(l => l.WorkingDir != null && l.WorkingDir != ""); + HasLinkedRepo = await ctx.Lists.AnyAsync(RepoLinkage.IsLinkedInDb); } finally { IsBusy = false; } } diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/SettingsModalViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/SettingsModalViewModelTests.cs index 1f3799bc..c802c58a 100644 --- a/tests/ClaudeDo.Ui.Tests/ViewModels/SettingsModalViewModelTests.cs +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/SettingsModalViewModelTests.cs @@ -167,6 +167,21 @@ public class SettingsModalViewModelTests : IDisposable Assert.True(vm.HasLinkedRepo); } + [Fact] + public async Task HasLinkedRepo_false_when_a_list_working_dir_is_whitespace_only() + { + using (var ctx = NewContext()) + { + ctx.Lists.Add(new ListEntity { Id = "l1", Name = "Blank repo", CreatedAt = DateTime.UtcNow, WorkingDir = " " }); + ctx.SaveChanges(); + } + var vm = MakeVm(new FakeWorker()); + + await vm.LoadAsync(); + + Assert.False(vm.HasLinkedRepo); + } + [Fact] public void SelectedCategory_updates_SelectedIndex_and_ignores_null() {