diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index d169e9a1..d3d94c95 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -805,7 +805,7 @@ "sessionSkillsTab": { "installed": "Installiert: {0}", "installFailed": "Installation fehlgeschlagen: {0}", "updated": "Aktualisiert.", "updateFailed": "Aktualisierung fehlgeschlagen: {0}", "removed": "Entfernt.", "removeFailed": "Entfernen fehlgeschlagen: {0}" }, "worktreesTab": { "workerOffline": "Worker offline.", "removed": "{0} Worktree(s) entfernt.", "blocked": "Zwangsentfernung nicht möglich: {0} Aufgabe(n) laufen noch. Brich sie zuerst ab.", "removedFrom": "{0} Worktree(s) von {1} Aufgabe(n) entfernt.", "cleanupFailed": "Aufräumen fehlgeschlagen: {0}", "resetFailed": "Zurücksetzen fehlgeschlagen: {0}" }, "worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "Liste", "cleanupFailed": "Aufräumen fehlgeschlagen.", "cleanupFailedDetailed": "Aufräumen fehlgeschlagen: {0}", "removed": "{0} Worktree(s) entfernt.", "discardConfirm": "Worktree für \"{0}\" verwerfen? Nicht committete Arbeit geht verloren.", "discardFailed": "Worktree konnte nicht verworfen werden.", "keepFailed": "Worktree konnte nicht behalten werden.", "cannotForceRunning": "Eine laufende Aufgabe kann nicht zwangsweise entfernt werden.", "forceRemoveFailed": "Zwangsentfernung fehlgeschlagen.", "forceRemoveFailedDetailed": "Zwangsentfernung fehlgeschlagen: {0}", "batchDone": "{0} gemergt, {1} zu lösen.", "openInExplorerFailed": "Pfad konnte nicht geöffnet werden: {0}" }, - "listSettings": { "untitled": "Unbenannt", "ticketProjectNone": "— keins —", "loadFailed": "Listeneinstellungen konnten nicht geladen werden: {0}", "ticketProjectsLoadFailed": "Ticket-Projekte konnten nicht geladen werden: {0}", "confirmDeleteFolder": "Ordner \"{0}\" löschen? Die Listen darin bleiben erhalten und wandern auf die oberste Ebene." }, + "listSettings": { "untitled": "Unbenannt", "ticketProjectNone": "— keins —", "loadFailed": "Listeneinstellungen konnten nicht geladen werden: {0}", "ticketProjectsLoadFailed": "Ticket-Projekte konnten nicht geladen werden: {0}", "confirmDeleteFolder": "Ordner \"{0}\" löschen? Die Listen darin bleiben erhalten und wandern auf die oberste Ebene.", "confirmDeleteList": "Liste \"{0}\" und alle ihre Aufgaben löschen? Das kann nicht rückgängig gemacht werden.", "deleteBlockedPlanning": "Diese Liste hat Planungssitzungen mit Kind-Aufgaben. Erst diese verwerfen, dann die Liste löschen.", "workingDirMissing": "Dieser Ordner existiert nicht.", "workingDirNotRepo": "Dieser Ordner ist noch kein Git-Repository. Führe darin zuerst `git init` aus, oder leere das Arbeitsverzeichnis, um dies als manuelle Liste zu speichern." }, "detailsIsland": { "verifyFailed": "Merge ist erfolgt, aber das Verifikationskommando der Liste ist fehlgeschlagen — die Aufgabe wurde nicht auf 'Erledigt' gesetzt.", "untrackedCollision": "Merge abgelehnt — er würde eine unversionierte Datei im Ziel-Arbeitsverzeichnis überschreiben.", "stopOffline": "Worker offline — Task kann nicht gestoppt werden.", "stopFailed": "Stoppen fehlgeschlagen: {0}", "enqueueFailed": "In die Warteschlange stellen fehlgeschlagen: {0}", "dequeueFailed": "Aus der Warteschlange entfernen fehlgeschlagen: {0}", "resetAndRetryFailed": "Zurücksetzen & erneut versuchen fehlgeschlagen: {0}", "toggleDoneFailed": "Erledigt-Status konnte nicht aktualisiert werden: {0}" }, "mergeSection": { "openWorktreeFailed": "Worktree konnte nicht geöffnet werden: {0}" }, "about": { "openPathFailed": "Pfad konnte nicht geöffnet werden: {0}" }, diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index 2184215e..db94367c 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -805,7 +805,7 @@ "sessionSkillsTab": { "installed": "Installed: {0}", "installFailed": "Install failed: {0}", "updated": "Updated.", "updateFailed": "Update failed: {0}", "removed": "Removed.", "removeFailed": "Remove failed: {0}" }, "worktreesTab": { "workerOffline": "Worker offline.", "removed": "Removed {0} worktree(s).", "blocked": "Cannot force-remove: {0} task(s) still running. Cancel them first.", "removedFrom": "Removed {0} worktree(s) from {1} task(s).", "cleanupFailed": "Cleanup failed: {0}", "resetFailed": "Reset failed: {0}" }, "worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "list", "cleanupFailed": "Cleanup failed.", "cleanupFailedDetailed": "Cleanup failed: {0}", "removed": "Removed {0} worktree(s).", "discardConfirm": "Discard the worktree for \"{0}\"? Uncommitted work will be lost.", "discardFailed": "Failed to discard worktree.", "keepFailed": "Failed to keep worktree.", "cannotForceRunning": "Cannot force-remove a running task.", "forceRemoveFailed": "Force remove failed.", "forceRemoveFailedDetailed": "Force remove failed: {0}", "batchDone": "Merged {0}, {1} need resolution.", "openInExplorerFailed": "Couldn't open path: {0}" }, - "listSettings": { "untitled": "Untitled", "ticketProjectNone": "— none —", "loadFailed": "Couldn't load list settings: {0}", "ticketProjectsLoadFailed": "Couldn't load ticket projects: {0}", "confirmDeleteFolder": "Delete folder \"{0}\"? The lists inside it are kept and move back to the top level." }, + "listSettings": { "untitled": "Untitled", "ticketProjectNone": "— none —", "loadFailed": "Couldn't load list settings: {0}", "ticketProjectsLoadFailed": "Couldn't load ticket projects: {0}", "confirmDeleteFolder": "Delete folder \"{0}\"? The lists inside it are kept and move back to the top level.", "confirmDeleteList": "Delete list \"{0}\" and all its tasks? This cannot be undone.", "deleteBlockedPlanning": "This list has planning sessions with child tasks. Discard those first, then delete the list.", "workingDirMissing": "This folder doesn't exist.", "workingDirNotRepo": "This folder isn't a git repository yet. Run `git init` in it first, or clear the working directory to save this as a manual list." }, "detailsIsland": { "verifyFailed": "Merge landed, but the list's verify command failed — the task was kept out of Done.", "untrackedCollision": "Merge refused — it would overwrite an untracked file in the target working directory.", "stopOffline": "Worker offline — can't stop the task.", "stopFailed": "Stop failed: {0}", "enqueueFailed": "Queue failed: {0}", "dequeueFailed": "Remove from queue failed: {0}", "resetAndRetryFailed": "Reset & retry failed: {0}", "toggleDoneFailed": "Couldn't update done state: {0}" }, "mergeSection": { "openWorktreeFailed": "Couldn't open worktree: {0}" }, "about": { "openPathFailed": "Couldn't open path: {0}" }, diff --git a/src/ClaudeDo.Ui/Services/RepoScanner.cs b/src/ClaudeDo.Ui/Services/RepoScanner.cs index ab25250a..aaa6fd76 100644 --- a/src/ClaudeDo.Ui/Services/RepoScanner.cs +++ b/src/ClaudeDo.Ui/Services/RepoScanner.cs @@ -69,5 +69,5 @@ public static class RepoScanner // A real clone has .git as a directory. Worktrees and submodules have it as a file // ("gitdir: ...") — those are not standalone repos and would flood the list. - private static bool IsRepo(string dir) => Directory.Exists(Path.Combine(dir, ".git")); + public static bool IsRepo(string dir) => Directory.Exists(Path.Combine(dir, ".git")); } diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs index 3c8e4763..791ae825 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs @@ -39,6 +39,11 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase [ObservableProperty] private string _name = ""; [ObservableProperty] private string _workingDir = ""; + // Recomputed whenever WorkingDir changes (typing or Browse) — RepoScanner.IsRepo is the same + // check RepoImport uses, so a folder either scans in there or warns here, never neither. + [ObservableProperty] + [NotifyCanExecuteChangedFor(nameof(SaveCommand))] + private string _workingDirWarning = ""; [ObservableProperty] private string _defaultCommitType = CommitTypeRegistry.DefaultType; // A manual list holds reminders: tasks created here start out manual (TaskEntity.IsManual). [ObservableProperty] private bool _isManual; @@ -250,9 +255,20 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase } } - private bool CanSave() => !IsLoading && !_loadFailed; + private bool CanSave() => !IsLoading && !_loadFailed && (IsFolder || string.IsNullOrEmpty(WorkingDirWarning)); private bool CanDelete() => !IsLoading; + // A manual list without a WorkingDir is untouched (empty is always valid) — this only fires + // for the "picked a folder that turns out not to be a repo" case the audit found: it would + // otherwise only surface as a Failed task the first time the worker tries to use it. + partial void OnWorkingDirChanged(string value) + { + if (string.IsNullOrWhiteSpace(value)) { WorkingDirWarning = ""; return; } + WorkingDirWarning = !Directory.Exists(value) + ? Loc.T("vm.listSettings.workingDirMissing") + : RepoScanner.IsRepo(value) ? "" : Loc.T("vm.listSettings.workingDirNotRepo"); + } + [RelayCommand(CanExecute = nameof(CanSave))] private async Task SaveAsync() { @@ -330,7 +346,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase var displayName = string.IsNullOrWhiteSpace(Name) ? Loc.T("vm.listSettings.untitled") : Name; if (ConfirmAsync is not null) { - var ok = await ConfirmAsync($"Delete list \"{displayName}\" and all its tasks? This cannot be undone."); + var ok = await ConfirmAsync(Loc.T("vm.listSettings.confirmDeleteList", displayName)); if (!ok) return; } @@ -347,7 +363,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase || ex.InnerException?.Message.Contains("FOREIGN KEY", StringComparison.OrdinalIgnoreCase) == true)) { if (ShowErrorAsync is not null) - await ShowErrorAsync("This list has planning sessions with child tasks. Discard those first, then delete the list."); + await ShowErrorAsync(Loc.T("vm.listSettings.deleteBlockedPlanning")); return; } diff --git a/src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml index f8bdae28..365111e5 100644 --- a/src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml @@ -94,6 +94,9 @@