diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json
index 8921d76e..9fe0e41f 100644
--- a/src/ClaudeDo.Localization/locales/de.json
+++ b/src/ClaudeDo.Localization/locales/de.json
@@ -357,7 +357,9 @@
"merge": "Mergen…",
"filesHeader": "Dateien",
"binary": "Binärdatei — kein Text-Diff",
- "empty": "Kein Inhalt"
+ "empty": "Kein Inhalt",
+ "splitView": "Nebeneinander",
+ "wrapLines": "Zeilenumbruch"
},
"worktreesOverview": {
"refresh": "Aktualisieren",
diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json
index 372d2d9a..328d7333 100644
--- a/src/ClaudeDo.Localization/locales/en.json
+++ b/src/ClaudeDo.Localization/locales/en.json
@@ -357,7 +357,9 @@
"merge": "Merge…",
"filesHeader": "Files",
"binary": "Binary file — no text diff",
- "empty": "No content"
+ "empty": "No content",
+ "splitView": "Side by side",
+ "wrapLines": "Wrap lines"
},
"worktreesOverview": {
"refresh": "Refresh",
diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/DiffViewerViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/DiffViewerViewModel.cs
index 7dda17d8..455e1be4 100644
--- a/src/ClaudeDo.Ui/ViewModels/Modals/DiffViewerViewModel.cs
+++ b/src/ClaudeDo.Ui/ViewModels/Modals/DiffViewerViewModel.cs
@@ -1,7 +1,10 @@
+using System;
using System.Collections.ObjectModel;
+using System.IO;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data.Git;
+using ClaudeDo.Ui;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
@@ -21,6 +24,7 @@ public sealed partial class DiffViewerViewModel : ViewModelBase
{
private readonly GitService _git;
private readonly IWorkerClient _worker;
+ private readonly AppSettings _settings;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsPlanning))]
@@ -56,6 +60,31 @@ public sealed partial class DiffViewerViewModel : ViewModelBase
[ObservableProperty] private string _displayedDiff = "";
[ObservableProperty] private string? _statusMessage;
+ // ── View toggles (persisted to ui.config.json) ──────────────────────────
+ [ObservableProperty] private bool _isSplitView;
+ [ObservableProperty] private bool _wrapLines;
+
+ partial void OnIsSplitViewChanged(bool value)
+ {
+ _settings.DiffViewMode = value ? "split" : "unified";
+ PersistViewPreferences();
+ }
+
+ partial void OnWrapLinesChanged(bool value)
+ {
+ _settings.DiffWrapLines = value;
+ PersistViewPreferences();
+ }
+
+ /// A failed preference write must never take the diff viewer down with it; the toggle
+ /// still works for this session, it just won't survive a restart.
+ private void PersistViewPreferences()
+ {
+ try { _settings.Save(); }
+ catch (IOException) { }
+ catch (UnauthorizedAccessException) { }
+ }
+
// ── Planning combined toggle ────────────────────────────────────────────
[ObservableProperty] private bool _isCombinedMode;
[ObservableProperty] private string? _combinedWarning;
@@ -63,10 +92,13 @@ public sealed partial class DiffViewerViewModel : ViewModelBase
public Action? CloseAction { get; set; }
- public DiffViewerViewModel(GitService git, IWorkerClient worker)
+ public DiffViewerViewModel(GitService git, IWorkerClient worker, AppSettings settings)
{
_git = git;
_worker = worker;
+ _settings = settings;
+ _isSplitView = string.Equals(settings.DiffViewMode, "split", StringComparison.OrdinalIgnoreCase);
+ _wrapLines = settings.DiffWrapLines;
}
[RelayCommand]
diff --git a/src/ClaudeDo.Ui/Views/Modals/DiffViewerView.axaml b/src/ClaudeDo.Ui/Views/Modals/DiffViewerView.axaml
index 990ac855..5d66da0c 100644
--- a/src/ClaudeDo.Ui/Views/Modals/DiffViewerView.axaml
+++ b/src/ClaudeDo.Ui/Views/Modals/DiffViewerView.axaml
@@ -30,6 +30,12 @@
+
+
+
+
+
+
@@ -149,10 +155,10 @@
Foreground="{DynamicResource TextMuteBrush}"
IsVisible="{Binding SelectedFile.IsEmptyContent}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
-
-
-
+
diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/DiffViewerViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/DiffViewerViewModelTests.cs
index c38275d1..5c27c6b5 100644
--- a/tests/ClaudeDo.Ui.Tests/ViewModels/DiffViewerViewModelTests.cs
+++ b/tests/ClaudeDo.Ui.Tests/ViewModels/DiffViewerViewModelTests.cs
@@ -17,6 +17,13 @@ public class DiffViewerViewModelTests
LocaleStore.Load(Path.Combine(dir!, "src", "ClaudeDo.Localization", "locales")), "en");
}
+ // A throwaway config path — the view toggles call Save(), and a test must never
+ // overwrite the developer's real ~/.todo-app/ui.config.json.
+ private static AppSettings TestSettings() => new()
+ {
+ ConfigPath = Path.Combine(Path.GetTempPath(), $"claudedo-uicfg-{Guid.NewGuid():N}.json"),
+ };
+
private sealed class FakePlanningWorker : StubWorkerClient
{
public IReadOnlyList AggregateResult { get; set; } = Array.Empty();
@@ -36,7 +43,7 @@ public class DiffViewerViewModelTests
[Fact]
public async Task CommitRange_NullHeadCommit_ShowsUnavailable()
{
- var vm = new DiffViewerViewModel(null!, new FakePlanningWorker());
+ var vm = new DiffViewerViewModel(null!, new FakePlanningWorker(), TestSettings());
vm.ConfigureCommitRange("/some/repo", "abc123", null);
await vm.LoadAsync();
@@ -49,7 +56,7 @@ public class DiffViewerViewModelTests
[Fact]
public async Task CommitRange_NullBaseRef_ShowsUnavailable()
{
- var vm = new DiffViewerViewModel(null!, new FakePlanningWorker());
+ var vm = new DiffViewerViewModel(null!, new FakePlanningWorker(), TestSettings());
vm.ConfigureCommitRange("/some/repo", null, "def456");
await vm.LoadAsync();
@@ -104,7 +111,7 @@ public class DiffViewerViewModelTests
new SubtaskDiffDto("s2", "Second", "branch-2", "base2", "head2", "+2 -1", "diff2"),
}
};
- var vm = new DiffViewerViewModel(null!, fake);
+ var vm = new DiffViewerViewModel(null!, fake, TestSettings());
vm.ConfigurePlanning("plan-1", "main");
await vm.LoadAsync();
@@ -124,7 +131,7 @@ public class DiffViewerViewModelTests
new SubtaskDiffDto("s2", "Second", "b2", "base2", "head2", null, "DIFF-B"),
}
};
- var vm = new DiffViewerViewModel(null!, fake);
+ var vm = new DiffViewerViewModel(null!, fake, TestSettings());
vm.ConfigurePlanning("plan-1", "main");
await vm.LoadAsync();
@@ -141,7 +148,7 @@ public class DiffViewerViewModelTests
AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A") },
CombinedResult = new CombinedDiffResultDto(true, "integration-branch", "COMBINED-DIFF", null, null),
};
- var vm = new DiffViewerViewModel(null!, fake);
+ var vm = new DiffViewerViewModel(null!, fake, TestSettings());
vm.ConfigurePlanning("plan-1", "main");
await vm.LoadAsync();
@@ -162,7 +169,7 @@ public class DiffViewerViewModelTests
AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A") },
CombinedResult = new CombinedDiffResultDto(false, null, null, "subtask-42", new[] { "a.cs", "b.cs" }),
};
- var vm = new DiffViewerViewModel(null!, fake);
+ var vm = new DiffViewerViewModel(null!, fake, TestSettings());
vm.ConfigurePlanning("plan-1", "main");
await vm.LoadAsync();
@@ -184,7 +191,7 @@ public class DiffViewerViewModelTests
AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A") },
CombinedResult = null,
};
- var vm = new DiffViewerViewModel(null!, fake);
+ var vm = new DiffViewerViewModel(null!, fake, TestSettings());
vm.ConfigurePlanning("plan-1", "main");
await vm.LoadAsync();
@@ -205,7 +212,7 @@ public class DiffViewerViewModelTests
AggregateResult = new[] { new SubtaskDiffDto("s1", "First", "b1", "base1", "head1", null, "DIFF-A") },
CombinedException = "planning task not found",
};
- var vm = new DiffViewerViewModel(null!, fake);
+ var vm = new DiffViewerViewModel(null!, fake, TestSettings());
vm.ConfigurePlanning("plan-1", "main");
await vm.LoadAsync();