diff --git a/src/ClaudeDo.Ui/Views/Controls/DiffTextView.axaml.cs b/src/ClaudeDo.Ui/Views/Controls/DiffTextView.axaml.cs
index b6670ebc..79786c49 100644
--- a/src/ClaudeDo.Ui/Views/Controls/DiffTextView.axaml.cs
+++ b/src/ClaudeDo.Ui/Views/Controls/DiffTextView.axaml.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
@@ -60,6 +61,24 @@ public partial class DiffTextView : UserControl
private bool _renderersInstalled;
+ // Above this many raw diff lines, DiffAlignment.Build moves to a background thread instead
+ // of running inline in ReloadFile. Build is O(lines) plus one word-diff LCS per 1:1 changed
+ // pair, and each LCS is up to MaxWordDiffTokens² (400² = 160k int cells, ~640KB) — a diff
+ // this size can plausibly stack up enough such pairs to blow a 16ms frame, and Planning mode
+ // instantiates one DiffTextView per changed file in an ItemsControl, so several can hit this
+ // at once. A line-count threshold was chosen over chunked/incremental alignment because Build
+ // is already pure and side-effect-free (safe to run on Task.Run as-is), whereas incremental
+ // construction would need to fragment its LCS/gap-detection state across calls for no benefit
+ // to the common case. Below the threshold, staying synchronous avoids a placeholder frame
+ // (there is nothing to show until the first build completes) for the overwhelming majority of
+ // diffs, which finish well under a frame either way.
+ private const int AsyncBuildLineThreshold = 500;
+
+ // Bumped by every ReloadFile call; a background build only applies its result if this is
+ // still the generation that started it, so a fast File-to-File switch during a slow build
+ // can't overwrite the newer file's rows with a stale AlignedDiff.
+ private int _buildGeneration;
+
public DiffTextView()
{
InitializeComponent();
@@ -97,9 +116,29 @@ public partial class DiffTextView : UserControl
/// Recomputes the alignment (word-diff LCS) for the current and then
/// refreshes everything downstream of it. Only ever needed when the file actually changes.
+ /// Small diffs build inline; large ones move to a background thread (see
+ /// ) and keep showing the previous file's rows until
+ /// the new build lands, rather than flashing an empty frame.
private void ReloadFile()
{
- _aligned = DiffAlignment.Build(File?.Lines);
+ var lines = File?.Lines;
+ var generation = ++_buildGeneration;
+
+ if (lines is null || lines.Count <= AsyncBuildLineThreshold)
+ {
+ _aligned = DiffAlignment.Build(lines);
+ RefreshLayout();
+ return;
+ }
+
+ _ = BuildAsync(lines, generation);
+ }
+
+ private async Task BuildAsync(IReadOnlyList lines, int generation)
+ {
+ var aligned = await Task.Run(() => DiffAlignment.Build(lines));
+ if (generation != _buildGeneration) return; // superseded by a newer File in the meantime.
+ _aligned = aligned;
RefreshLayout();
}