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:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user