feat(ui): replay persisted task log when selecting a task

Read the task's LogPath on selection and feed each line through the
live-stream parser so Claude output stays visible across app restarts.
Tail-caps at 2000 lines to avoid flooding the UI.
This commit is contained in:
mika kuns
2026-04-23 13:07:54 +02:00
parent 6f725d12f5
commit c8c8bb4a47

View File

@@ -378,6 +378,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
// Subscribe only after DB load confirms the task exists // Subscribe only after DB load confirms the task exists
_subscribedTaskId = row.Id; _subscribedTaskId = row.Id;
// Replay the latest run's persisted log so output is visible across app restarts.
await ReplayLogFileAsync(entity.LogPath, ct);
var subs = await subtaskRepo.GetByTaskIdAsync(row.Id); var subs = await subtaskRepo.GetByTaskIdAsync(row.Id);
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
foreach (var s in subs) foreach (var s in subs)
@@ -386,6 +389,59 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
catch (OperationCanceledException) { } catch (OperationCanceledException) { }
} }
private async System.Threading.Tasks.Task ReplayLogFileAsync(string? logPath, CancellationToken ct)
{
if (string.IsNullOrWhiteSpace(logPath)) return;
var expanded = ExpandUserPath(logPath);
if (!System.IO.File.Exists(expanded)) return;
try
{
const int maxLines = 2000;
string[] all;
await using (var fs = new System.IO.FileStream(
expanded,
System.IO.FileMode.Open,
System.IO.FileAccess.Read,
System.IO.FileShare.ReadWrite | System.IO.FileShare.Delete))
using (var reader = new System.IO.StreamReader(fs))
{
var list = new List<string>();
while (await reader.ReadLineAsync(ct) is { } line)
list.Add(line);
all = list.ToArray();
}
ct.ThrowIfCancellationRequested();
var start = Math.Max(0, all.Length - maxLines);
for (int i = start; i < all.Length; i++)
{
ct.ThrowIfCancellationRequested();
if (_subscribedTaskId is null) return;
// Worker writes raw Claude CLI stdout to disk (no prefix) but broadcasts
// it with a "[stdout] " prefix. Match the live-stream format so the same
// stream-json parser handles both.
var line = all[i];
var normalized = line.StartsWith("[", StringComparison.Ordinal) ? line : "[stdout] " + line;
OnTaskMessage(_subscribedTaskId, normalized);
}
FlushClaudeBuffer();
}
catch (OperationCanceledException) { throw; }
catch { /* best-effort replay */ }
}
private static string ExpandUserPath(string path)
{
if (path.StartsWith("~/", StringComparison.Ordinal) || path.StartsWith("~\\", StringComparison.Ordinal))
return System.IO.Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
path[2..]);
if (path == "~")
return Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
return path;
}
private async System.Threading.Tasks.Task RefreshWorktreeAsync(string taskId) private async System.Threading.Tasks.Task RefreshWorktreeAsync(string taskId)
{ {
try try