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)