diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TaskMonitorViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TaskMonitorViewModel.cs index 35b39472..147c064a 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TaskMonitorViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TaskMonitorViewModel.cs @@ -1,5 +1,6 @@ using System.Collections.ObjectModel; using System.Text; +using System.Text.Json; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using ClaudeDo.Data; @@ -258,16 +259,38 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IMissionContro var idx = result.IndexOf(RoadblockMarker, StringComparison.Ordinal); if (idx < 0) { - SessionOutcome = result; + SessionOutcome = UnwrapSummary(result); Roadblocks = null; return; } var summary = result[..idx].TrimEnd().TrimEnd('⚠').TrimEnd(); - SessionOutcome = string.IsNullOrWhiteSpace(summary) ? null : summary; + SessionOutcome = string.IsNullOrWhiteSpace(summary) ? null : UnwrapSummary(summary); Roadblocks = result[(idx + RoadblockMarker.Length)..].Trim(); } + /// Safety net for older rows persisted as raw structured-output JSON: if the outcome + /// is a JSON object with a string summary, show that; otherwise keep the text as-is. + private static string? UnwrapSummary(string? text) + { + if (string.IsNullOrWhiteSpace(text)) return text; + var trimmed = text.TrimStart(); + if (trimmed.Length == 0 || trimmed[0] != '{') return text; + try + { + using var doc = JsonDocument.Parse(trimmed); + if (doc.RootElement.ValueKind == JsonValueKind.Object && + doc.RootElement.TryGetProperty("summary", out var s) && + s.ValueKind == JsonValueKind.String) + { + var summary = s.GetString(); + if (!string.IsNullOrWhiteSpace(summary)) return summary; + } + } + catch { } + return text; + } + public async System.Threading.Tasks.Task ReplayLogFileAsync(string? logPath, CancellationToken ct) { if (string.IsNullOrWhiteSpace(logPath)) return; diff --git a/src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs b/src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs index 6b683d06..c96f4dcd 100644 --- a/src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs +++ b/src/ClaudeDo.Worker/Runner/StreamAnalyzer.cs @@ -91,16 +91,34 @@ public sealed class StreamAnalyzer private string? FallbackResult() { + // A --json-schema run sometimes echoes the structured JSON as the result text; unwrap + // that to its summary. A plain markdown result is kept verbatim (even when a separate + // structured_output is also present). Only when there is no result text do we fall back + // to the structured summary, then the raw JSON. + if (SummaryFromJson(_resultMarkdown) is { } fromResult) return fromResult; if (!string.IsNullOrEmpty(_resultMarkdown)) return _resultMarkdown; - if (_structuredOutputJson is null) return _resultMarkdown; + if (SummaryFromJson(_structuredOutputJson) is { } fromStructured) return fromStructured; + return _structuredOutputJson; + } + + /// Extracts a non-empty string summary from a JSON object, or null when + /// the input isn't such an object (a plain markdown result parses to null and is kept). + private static string? SummaryFromJson(string? json) + { + if (string.IsNullOrWhiteSpace(json)) return null; try { - using var doc = JsonDocument.Parse(_structuredOutputJson); - if (doc.RootElement.TryGetProperty("summary", out var s)) - return s.GetString(); + using var doc = JsonDocument.Parse(json); + if (doc.RootElement.ValueKind == JsonValueKind.Object && + doc.RootElement.TryGetProperty("summary", out var s) && + s.ValueKind == JsonValueKind.String) + { + var text = s.GetString(); + return string.IsNullOrWhiteSpace(text) ? null : text; + } } catch { } - return _structuredOutputJson; + return null; } private void CollectFromAssistant(JsonElement root)