diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json
index 458872dc..2d03796d 100644
--- a/src/ClaudeDo.Localization/locales/de.json
+++ b/src/ClaudeDo.Localization/locales/de.json
@@ -221,8 +221,8 @@
"rerun": "Erneut ausführen",
"refineTip": "Aufgabe mit Claude verfeinern",
"refiningTip": "Claude verfeinert diese Aufgabe…",
- "emptyHint": "Noch keine Aufgaben. Oben eine hinzufügen oder in die Warteschlange einreihen, wenn du bereit bist.",
- "emptyHintNoWorkingDir": "Verknüpfe ein Repo in den Listeneinstellungen, um Warteschlange, „Claude machen lassen" und die Schnell-Sitzung zu aktivieren."
+ "emptyHint": "Noch keine Aufgaben. Lege eine an und wähle per Rechtsklick „In Warteschlange einreihen" — Claude bearbeitet sie in einem eigenen Worktree.",
+ "emptyHintNoWorkingDir": "Diese Liste hat kein Repo — in den Listeneinstellungen verknüpfen, um die Warteschlange zu aktivieren."
},
"lists": {
"heading": "Listen",
@@ -237,7 +237,9 @@
"contextOpenFindings": "Findings-Ordner öffnen",
"contextSettings": "Einstellungen…",
"newList": "Neue Liste",
- "addReposTip": "Repos als Listen hinzufügen"
+ "addReposTip": "Repos als Listen hinzufügen",
+ "noRepoBannerText": "Noch kein Repo verknüpft. ClaudeDo führt Tasks in einem Git-Repo aus.",
+ "noRepoBannerButton": "Repo importieren"
},
"details": {
"deleteTaskTip": "Aufgabe löschen",
@@ -309,6 +311,7 @@
"reviewContinueTip": "Dieses Feedback senden und die Aufgabe erneut ausführen",
"reviewResetTip": "Alle Änderungen verwerfen und die Aufgabe auf Leerlauf zurücksetzen",
"mergeDrainHint": "Merge zieht Subtasks ein — Abbrechen entsperrt sich danach",
+ "approveMergesUnitHint": "Approve mergt das gleich in den Zielbranch — danach folgt kein separater Merge-Schritt.",
"continueNoSessionTip": "Setzt den letzten Lauf fort — benötigt eine vorherige Session",
"resetAndRetryInteractiveTip": "Interaktive Sitzung ist offen — erst schließen",
"reviewFeedbackPlaceholder": "Feedback für nächsten Lauf"
diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json
index 7d2dd51e..c92cf11b 100644
--- a/src/ClaudeDo.Localization/locales/en.json
+++ b/src/ClaudeDo.Localization/locales/en.json
@@ -221,8 +221,8 @@
"rerun": "Re-run",
"refineTip": "Refine this task with Claude",
"refiningTip": "Claude is refining this task…",
- "emptyHint": "No tasks yet. Add one above, or send it to the queue once you're ready.",
- "emptyHintNoWorkingDir": "Link a repo in List Settings to enable the queue, Let Claude handle it, and Quick session."
+ "emptyHint": "No tasks yet. Add one, then right-click it and choose \"Send to queue\" — Claude works it in its own worktree.",
+ "emptyHintNoWorkingDir": "This list has no repo yet — link one in List Settings to enable the queue."
},
"lists": {
"heading": "Lists",
@@ -237,7 +237,9 @@
"contextOpenFindings": "Open findings folder",
"contextSettings": "Settings…",
"newList": "New list",
- "addReposTip": "Add repos as lists"
+ "addReposTip": "Add repos as lists",
+ "noRepoBannerText": "No repo linked yet. ClaudeDo runs tasks in a git repo.",
+ "noRepoBannerButton": "Import repo"
},
"details": {
"deleteTaskTip": "Delete task",
@@ -310,6 +312,7 @@
"reviewResetTip": "Discard all changes and reset the task to Idle",
"reviewFeedbackPlaceholder": "Feedback for next run",
"mergeDrainHint": "Merge is draining subtasks — Cancel unlocks once it finishes",
+ "approveMergesUnitHint": "Approve also merges this into the target branch right away — there's no separate merge step after it.",
"continueNoSessionTip": "Resumes the last run — needs a previous session to continue",
"resetAndRetryInteractiveTip": "Interactive session is open — close it first"
},
diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs
index 414e7bfd..ebeba0de 100644
--- a/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs
@@ -186,6 +186,14 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
[ObservableProperty] private string _searchText = "";
[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.
+ [ObservableProperty] private bool _hasNoLinkedRepo = true;
+
+ private void RecomputeHasNoLinkedRepo() =>
+ HasNoLinkedRepo = UserLists.All(r => string.IsNullOrWhiteSpace(r.WorkingDir));
+
public string UserName { get; } = Environment.UserName;
public string MachineName { get; } = Environment.MachineName;
public string MachineNameLocal => Loc.T("vm.lists.localSuffix", MachineName);
@@ -291,6 +299,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
}
finally { OperationTiming.Shared.Record("db", "ListsIsland.LoadAsync", sw.Elapsed, ok); }
+ RecomputeHasNoLinkedRepo();
await RefreshCountsAsync(ct);
SelectedList = Items.FirstOrDefault();
}
@@ -372,6 +381,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
Items.Add(item);
UserLists.Add(item);
SelectedList = item;
+ RecomputeHasNoLinkedRepo();
if (Dialogs is not null && _services is not null)
{
@@ -474,6 +484,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
row.DefaultCommitType = entity.DefaultCommitType;
row.IsManual = entity.IsManual;
row.FindingsTracked = entity.FindingsTracked;
+ RecomputeHasNoLinkedRepo();
}
catch { /* best-effort refresh */ }
}
@@ -505,5 +516,6 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
UserLists.Insert(insertAt, item);
Items.Insert(SmartLists.Count + insertAt, item);
+ RecomputeHasNoLinkedRepo();
}
}
diff --git a/src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml b/src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml
index 572aee89..3af8552c 100644
--- a/src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml
+++ b/src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml
@@ -371,13 +371,16 @@
IsVisible="{Binding Merge.ShowMergeSection}" />
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
()
+ .UseSqlite($"Data Source={_dbPath}")
+ .Options;
+ return new ClaudeDoDbContext(opts);
+ }
+
+ private sealed class TestDbFactory : IDbContextFactory
+ {
+ private readonly Func _create;
+ public TestDbFactory(Func create) => _create = create;
+ public ClaudeDoDbContext CreateDbContext() => _create();
+ }
+
+ private sealed class FakeWorker : StubWorkerClient
+ {
+ public override bool IsConnected => true;
+ }
+
+ private async Task SeedListAsync(string id, string name, string? workingDir = null)
+ {
+ await using var db = NewContext();
+ var entity = new ListEntity { Id = id, Name = name, WorkingDir = workingDir, CreatedAt = DateTime.UtcNow };
+ db.Lists.Add(entity);
+ await db.SaveChangesAsync();
+ return entity;
+ }
+
+ [Fact]
+ public async Task NoLists_HasNoLinkedRepo_IsTrue()
+ {
+ var vm = new ListsIslandViewModel(new TestDbFactory(NewContext));
+ await vm.LoadAsync();
+
+ Assert.True(vm.HasNoLinkedRepo);
+ }
+
+ [Fact]
+ public async Task OnlyNonUserSeedLists_HasNoLinkedRepo_IsTrue()
+ {
+ // "My Day"/"Important"/"Planned" are excluded from UserLists by name — they must not
+ // count as a linked repo even if (hypothetically) one had a WorkingDir.
+ await SeedListAsync("seed-my-day", "My Day", workingDir: @"C:\some\repo");
+
+ var vm = new ListsIslandViewModel(new TestDbFactory(NewContext));
+ await vm.LoadAsync();
+
+ Assert.Empty(vm.UserLists);
+ Assert.True(vm.HasNoLinkedRepo);
+ }
+
+ [Fact]
+ public async Task UserListWithoutWorkingDir_HasNoLinkedRepo_IsTrue()
+ {
+ await SeedListAsync("no-dir", "No Dir");
+
+ var vm = new ListsIslandViewModel(new TestDbFactory(NewContext));
+ await vm.LoadAsync();
+
+ Assert.True(vm.HasNoLinkedRepo);
+ }
+
+ [Fact]
+ public async Task UserListWithWorkingDir_HasNoLinkedRepo_IsFalse()
+ {
+ await SeedListAsync("has-dir", "Has Dir", workingDir: @"C:\some\repo");
+
+ var vm = new ListsIslandViewModel(new TestDbFactory(NewContext));
+ await vm.LoadAsync();
+
+ Assert.False(vm.HasNoLinkedRepo);
+ }
+
+ [Fact]
+ public async Task CreateListAsync_OnEmptyBoard_KeepsHasNoLinkedRepoTrue()
+ {
+ var vm = new ListsIslandViewModel(new TestDbFactory(NewContext));
+ await vm.LoadAsync();
+ Assert.True(vm.HasNoLinkedRepo);
+
+ // No Dialogs/service provider wired — CreateListAsync must still recompute the flag from
+ // the newly-added (working-dir-less) row, not leave it stale.
+ await vm.CreateListCommand.ExecuteAsync(null);
+
+ Assert.True(vm.HasNoLinkedRepo);
+ }
+
+ [Fact]
+ public async Task DeletingTheOnlyLinkedRepoList_ThenReloading_RecomputesToTrue()
+ {
+ var entity = await SeedListAsync("has-dir", "Has Dir", workingDir: @"C:\some\repo");
+
+ var vm = new ListsIslandViewModel(new TestDbFactory(NewContext));
+ await vm.LoadAsync();
+ Assert.False(vm.HasNoLinkedRepo);
+
+ // List Settings' delete path removes the DB row, then the caller reloads (see
+ // OpenListSettingsAsync/CreateListAsync: `if (vm.Deleted) await LoadAsync();`).
+ await using (var db = NewContext())
+ {
+ db.Lists.Remove(db.Lists.Find(entity.Id)!);
+ await db.SaveChangesAsync();
+ }
+ await vm.LoadAsync();
+
+ Assert.True(vm.HasNoLinkedRepo);
+ }
+
+ [Fact]
+ public async Task ListSettingsSave_LinkingWorkingDir_RecomputesToFalse()
+ {
+ var entity = await SeedListAsync("no-dir-yet", "No Dir Yet");
+
+ var worker = new FakeWorker();
+ var vm = new ListsIslandViewModel(new TestDbFactory(NewContext), worker: worker);
+ await vm.LoadAsync();
+ Assert.True(vm.HasNoLinkedRepo);
+
+ // Simulate a List Settings save linking a working dir — the worker broadcasts the same
+ // ListUpdated event a real save triggers, driving RefreshRowAsync.
+ await using (var db = NewContext())
+ {
+ var row = await db.Lists.FindAsync(entity.Id);
+ row!.WorkingDir = @"C:\some\repo";
+ await db.SaveChangesAsync();
+ }
+ worker.RaiseListUpdated(entity.Id);
+ await Task.Delay(100);
+
+ Assert.False(vm.HasNoLinkedRepo);
+ }
+
+ [Fact]
+ public async Task ListSettingsSave_RemovingLastWorkingDir_RecomputesToTrue()
+ {
+ var entity = await SeedListAsync("has-dir", "Has Dir", workingDir: @"C:\some\repo");
+
+ var worker = new FakeWorker();
+ var vm = new ListsIslandViewModel(new TestDbFactory(NewContext), worker: worker);
+ await vm.LoadAsync();
+ Assert.False(vm.HasNoLinkedRepo);
+
+ await using (var db = NewContext())
+ {
+ var row = await db.Lists.FindAsync(entity.Id);
+ row!.WorkingDir = null;
+ await db.SaveChangesAsync();
+ }
+ worker.RaiseListUpdated(entity.Id);
+ await Task.Delay(100);
+
+ Assert.True(vm.HasNoLinkedRepo);
+ }
+}