chore(claude-do): merge Actual highlighting in the Autonomus agount output window
ClaudeDo-Task: 87096034c52f4c869fb1518572c7f49f
This commit is contained in:
@@ -32,6 +32,8 @@ public sealed class LogKindForegroundConverter : IValueConverter
|
|||||||
LogKind.Done => "MossBrightBrush",
|
LogKind.Done => "MossBrightBrush",
|
||||||
LogKind.Msg => "TextDimBrush",
|
LogKind.Msg => "TextDimBrush",
|
||||||
LogKind.User => "AccentBrush",
|
LogKind.User => "AccentBrush",
|
||||||
|
LogKind.Edit => "AmberBrush",
|
||||||
|
LogKind.ToolResult => "TextMuteBrush",
|
||||||
_ => "TextDimBrush",
|
_ => "TextDimBrush",
|
||||||
} : "TextDimBrush";
|
} : "TextDimBrush";
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Helpers;
|
namespace ClaudeDo.Ui.Helpers;
|
||||||
|
|
||||||
|
/// <summary>Turns Claude CLI stream-json lines into kind-tagged log entries for the terminal views.</summary>
|
||||||
public class StreamLineFormatter
|
public class StreamLineFormatter
|
||||||
{
|
{
|
||||||
private const int MaxLength = 50_000;
|
private const int MaxLength = 50_000;
|
||||||
private const int MaxArgChars = 120;
|
private const int MaxArgChars = 500;
|
||||||
|
|
||||||
public string? FormatLine(string line)
|
/// <summary>Formats one stdout line; null when the event is filtered. Entry text may span several lines.</summary>
|
||||||
|
public IReadOnlyList<(LogKind Kind, string Text)>? FormatLine(string line)
|
||||||
{
|
{
|
||||||
JsonDocument doc;
|
JsonDocument doc;
|
||||||
try
|
try
|
||||||
@@ -17,7 +20,7 @@ public class StreamLineFormatter
|
|||||||
}
|
}
|
||||||
catch (JsonException)
|
catch (JsonException)
|
||||||
{
|
{
|
||||||
return line;
|
return [(LogKind.Stdout, line)];
|
||||||
}
|
}
|
||||||
|
|
||||||
using (doc)
|
using (doc)
|
||||||
@@ -28,7 +31,7 @@ public class StreamLineFormatter
|
|||||||
if (!root.TryGetProperty("type", out var typeProp))
|
if (!root.TryGetProperty("type", out var typeProp))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
return typeProp.GetString() switch
|
var entries = typeProp.GetString() switch
|
||||||
{
|
{
|
||||||
"system" => FormatSystem(root),
|
"system" => FormatSystem(root),
|
||||||
"assistant" => FormatAssistant(root),
|
"assistant" => FormatAssistant(root),
|
||||||
@@ -36,10 +39,11 @@ public class StreamLineFormatter
|
|||||||
"result" => FormatResult(root),
|
"result" => FormatResult(root),
|
||||||
_ => null,
|
_ => null,
|
||||||
};
|
};
|
||||||
|
return entries is { Count: > 0 } ? entries : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? FormatSystem(JsonElement root)
|
private static List<(LogKind, string)>? FormatSystem(JsonElement root)
|
||||||
{
|
{
|
||||||
if (!root.TryGetProperty("subtype", out var subtypeProp))
|
if (!root.TryGetProperty("subtype", out var subtypeProp))
|
||||||
return null;
|
return null;
|
||||||
@@ -48,7 +52,7 @@ public class StreamLineFormatter
|
|||||||
switch (subtype)
|
switch (subtype)
|
||||||
{
|
{
|
||||||
case "api_retry":
|
case "api_retry":
|
||||||
return "[Retrying API call...]\n";
|
return [(LogKind.Sys, "[Retrying API call...]")];
|
||||||
|
|
||||||
case "init":
|
case "init":
|
||||||
{
|
{
|
||||||
@@ -61,7 +65,7 @@ public class StreamLineFormatter
|
|||||||
? sessionId[..8]
|
? sessionId[..8]
|
||||||
: sessionId ?? "?";
|
: sessionId ?? "?";
|
||||||
var modelPart = string.IsNullOrEmpty(model) ? "" : $" · {model}";
|
var modelPart = string.IsNullOrEmpty(model) ? "" : $" · {model}";
|
||||||
return $"[session {shortId}{modelPart}]\n";
|
return [(LogKind.Sys, $"[session {shortId}{modelPart}]")];
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@@ -69,12 +73,12 @@ public class StreamLineFormatter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? FormatAssistant(JsonElement root)
|
private static List<(LogKind, string)>? FormatAssistant(JsonElement root)
|
||||||
{
|
{
|
||||||
if (!TryGetContentArray(root, out var content))
|
if (!TryGetContentArray(root, out var content))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var entries = new List<(LogKind, string)>();
|
||||||
foreach (var block in content.EnumerateArray())
|
foreach (var block in content.EnumerateArray())
|
||||||
{
|
{
|
||||||
if (block.ValueKind != JsonValueKind.Object) continue;
|
if (block.ValueKind != JsonValueKind.Object) continue;
|
||||||
@@ -87,16 +91,12 @@ public class StreamLineFormatter
|
|||||||
{
|
{
|
||||||
var text = textProp.GetString();
|
var text = textProp.GetString();
|
||||||
if (!string.IsNullOrEmpty(text))
|
if (!string.IsNullOrEmpty(text))
|
||||||
{
|
entries.Add((LogKind.Claude, text));
|
||||||
sb.Append(text);
|
|
||||||
if (!text.EndsWith('\n')) sb.Append('\n');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "tool_use":
|
case "tool_use":
|
||||||
sb.Append(FormatToolUse(block));
|
entries.Add(FormatToolUse(block));
|
||||||
sb.Append('\n');
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "thinking":
|
case "thinking":
|
||||||
@@ -106,7 +106,7 @@ public class StreamLineFormatter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sb.Length == 0 ? null : sb.ToString();
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryGetContentArray(JsonElement root, out JsonElement content)
|
private static bool TryGetContentArray(JsonElement root, out JsonElement content)
|
||||||
@@ -120,35 +120,30 @@ public class StreamLineFormatter
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? FormatUser(JsonElement root)
|
private static List<(LogKind, string)>? FormatUser(JsonElement root)
|
||||||
{
|
{
|
||||||
if (!TryGetContentArray(root, out var content))
|
if (!TryGetContentArray(root, out var content))
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var entries = new List<(LogKind, string)>();
|
||||||
foreach (var block in content.EnumerateArray())
|
foreach (var block in content.EnumerateArray())
|
||||||
{
|
{
|
||||||
if (block.ValueKind != JsonValueKind.Object) continue;
|
if (block.ValueKind != JsonValueKind.Object) continue;
|
||||||
if (!block.TryGetProperty("type", out var blockTypeProp)) continue;
|
if (!block.TryGetProperty("type", out var blockTypeProp)) continue;
|
||||||
if (blockTypeProp.GetString() != "tool_result") continue;
|
if (blockTypeProp.GetString() != "tool_result") continue;
|
||||||
|
|
||||||
var summary = BuildToolResultSummary(root, block);
|
|
||||||
if (!string.IsNullOrEmpty(summary))
|
|
||||||
{
|
|
||||||
sb.Append("→ ");
|
|
||||||
sb.Append(summary);
|
|
||||||
sb.Append('\n');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return sb.Length == 0 ? null : sb.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string BuildToolResultSummary(JsonElement root, JsonElement block)
|
|
||||||
{
|
|
||||||
var isError = block.TryGetProperty("is_error", out var errProp)
|
var isError = block.TryGetProperty("is_error", out var errProp)
|
||||||
&& errProp.ValueKind == JsonValueKind.True;
|
&& errProp.ValueKind == JsonValueKind.True;
|
||||||
|
var summary = BuildToolResultSummary(root, block, isError);
|
||||||
|
if (!string.IsNullOrEmpty(summary))
|
||||||
|
entries.Add((isError ? LogKind.Stderr : LogKind.ToolResult, "→ " + summary));
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildToolResultSummary(JsonElement root, JsonElement block, bool isError)
|
||||||
|
{
|
||||||
var contentText = ResolveContentText(block);
|
var contentText = ResolveContentText(block);
|
||||||
|
|
||||||
if (isError)
|
if (isError)
|
||||||
@@ -215,14 +210,14 @@ public class StreamLineFormatter
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? FormatResult(JsonElement root)
|
private static List<(LogKind, string)>? FormatResult(JsonElement root)
|
||||||
{
|
{
|
||||||
if (root.TryGetProperty("result", out var resultProp))
|
if (root.TryGetProperty("result", out var resultProp))
|
||||||
return $"\n--- Result ---\n{resultProp.GetString()}\n";
|
return [(LogKind.Done, $"--- Result ---\n{resultProp.GetString()}")];
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string FormatToolUse(JsonElement block)
|
private static (LogKind, string) FormatToolUse(JsonElement block)
|
||||||
{
|
{
|
||||||
var name = block.TryGetProperty("name", out var nameProp)
|
var name = block.TryGetProperty("name", out var nameProp)
|
||||||
? nameProp.GetString() ?? "?"
|
? nameProp.GetString() ?? "?"
|
||||||
@@ -241,10 +236,9 @@ public class StreamLineFormatter
|
|||||||
}
|
}
|
||||||
|
|
||||||
string? arg = hasInput ? BuildToolArg(name, input) : null;
|
string? arg = hasInput ? BuildToolArg(name, input) : null;
|
||||||
|
var kind = name is "Edit" or "Write" or "NotebookEdit" ? LogKind.Edit : LogKind.Tool;
|
||||||
|
|
||||||
return string.IsNullOrEmpty(arg)
|
return (kind, string.IsNullOrEmpty(arg) ? $"[{label}]" : $"[{label}] {arg}");
|
||||||
? $"[{label}]"
|
|
||||||
: $"[{label}] {arg}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? BuildToolArg(string toolName, JsonElement input)
|
private static string? BuildToolArg(string toolName, JsonElement input)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
namespace ClaudeDo.Ui.ViewModels.Islands;
|
namespace ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
|
||||||
public enum LogKind { Sys, Tool, Claude, Stdout, Stderr, Done, Msg, User }
|
public enum LogKind { Sys, Tool, Claude, Stdout, Stderr, Done, Msg, User, Edit, ToolResult }
|
||||||
|
|
||||||
public sealed class LogLineViewModel
|
public sealed class LogLineViewModel
|
||||||
{
|
{
|
||||||
@@ -17,6 +17,8 @@ public sealed class LogLineViewModel
|
|||||||
LogKind.Done => "done",
|
LogKind.Done => "done",
|
||||||
LogKind.Msg => "claude",
|
LogKind.Msg => "claude",
|
||||||
LogKind.User => "you",
|
LogKind.User => "you",
|
||||||
|
LogKind.Edit => "edit",
|
||||||
|
LogKind.ToolResult => "result",
|
||||||
_ => "",
|
_ => "",
|
||||||
};
|
};
|
||||||
public string ClassName => Kind switch
|
public string ClassName => Kind switch
|
||||||
@@ -29,6 +31,20 @@ public sealed class LogLineViewModel
|
|||||||
LogKind.Done => "log-done",
|
LogKind.Done => "log-done",
|
||||||
LogKind.Msg => "log-msg",
|
LogKind.Msg => "log-msg",
|
||||||
LogKind.User => "log-user",
|
LogKind.User => "log-user",
|
||||||
|
LogKind.Edit => "log-edit",
|
||||||
|
LogKind.ToolResult => "log-result",
|
||||||
_ => "",
|
_ => "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// <summary>Splits formatter entries into one log line per non-blank text line, keeping each entry's kind.</summary>
|
||||||
|
public static IEnumerable<LogLineViewModel> FromEntries(IEnumerable<(LogKind Kind, string Text)> entries)
|
||||||
|
{
|
||||||
|
foreach (var (kind, text) in entries)
|
||||||
|
foreach (var raw in text.Split('\n'))
|
||||||
|
{
|
||||||
|
var piece = raw.TrimEnd('\r');
|
||||||
|
if (!string.IsNullOrWhiteSpace(piece))
|
||||||
|
yield return new LogLineViewModel { Kind = kind, Text = piece };
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Text;
|
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using ClaudeDo.Ui.Helpers;
|
using ClaudeDo.Ui.Helpers;
|
||||||
@@ -13,7 +12,6 @@ public sealed partial class PrepPanelViewModel : ViewModelBase, IDisposable
|
|||||||
{
|
{
|
||||||
private readonly IWorkerClient _worker;
|
private readonly IWorkerClient _worker;
|
||||||
private readonly StreamLineFormatter _formatter = new();
|
private readonly StreamLineFormatter _formatter = new();
|
||||||
private readonly StringBuilder _prepClaudeBuf = new();
|
|
||||||
|
|
||||||
private readonly Action _onPrepStartedHandler;
|
private readonly Action _onPrepStartedHandler;
|
||||||
private readonly Action<string> _onPrepLineHandler;
|
private readonly Action<string> _onPrepLineHandler;
|
||||||
@@ -128,24 +126,8 @@ public sealed partial class PrepPanelViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
private void AppendStdoutLine(string line)
|
private void AppendStdoutLine(string line)
|
||||||
{
|
{
|
||||||
var formatted = _formatter.FormatLine(line);
|
if (_formatter.FormatLine(line) is not { } entries) return;
|
||||||
if (formatted is null) return;
|
foreach (var entry in LogLineViewModel.FromEntries(entries))
|
||||||
AppendClaudeText(formatted);
|
PrepLog.Add(entry);
|
||||||
}
|
|
||||||
|
|
||||||
private void AppendClaudeText(string chunk)
|
|
||||||
{
|
|
||||||
_prepClaudeBuf.Append(chunk);
|
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var text = _prepClaudeBuf.ToString();
|
|
||||||
var nl = text.IndexOf('\n');
|
|
||||||
if (nl < 0) break;
|
|
||||||
var piece = text[..nl].TrimEnd('\r');
|
|
||||||
if (!string.IsNullOrWhiteSpace(piece))
|
|
||||||
PrepLog.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
|
|
||||||
_prepClaudeBuf.Clear();
|
|
||||||
_prepClaudeBuf.Append(text[(nl + 1)..]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Text;
|
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
@@ -18,7 +17,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
|||||||
private readonly IWorkerClient _worker;
|
private readonly IWorkerClient _worker;
|
||||||
|
|
||||||
private readonly StreamLineFormatter _formatter = new();
|
private readonly StreamLineFormatter _formatter = new();
|
||||||
private readonly StringBuilder _claudeBuf = new();
|
|
||||||
private string? _subscribedTaskId;
|
private string? _subscribedTaskId;
|
||||||
|
|
||||||
public string? SubscribedTaskId => _subscribedTaskId;
|
public string? SubscribedTaskId => _subscribedTaskId;
|
||||||
@@ -105,7 +103,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
|||||||
_onTaskFinished = (slot, taskId, status, finishedAt) =>
|
_onTaskFinished = (slot, taskId, status, finishedAt) =>
|
||||||
{
|
{
|
||||||
if (taskId != _subscribedTaskId) return;
|
if (taskId != _subscribedTaskId) return;
|
||||||
FlushClaudeBuffer();
|
|
||||||
Log.Add(new LogLineViewModel
|
Log.Add(new LogLineViewModel
|
||||||
{
|
{
|
||||||
Kind = LogKind.Done,
|
Kind = LogKind.Done,
|
||||||
@@ -182,7 +179,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
|||||||
public void Reset()
|
public void Reset()
|
||||||
{
|
{
|
||||||
Log.Clear();
|
Log.Clear();
|
||||||
_claudeBuf.Clear();
|
|
||||||
_subscribedTaskId = null;
|
_subscribedTaskId = null;
|
||||||
AgentState = "idle";
|
AgentState = "idle";
|
||||||
SessionOutcome = null;
|
SessionOutcome = null;
|
||||||
@@ -272,7 +268,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
|||||||
var normalized = line.StartsWith("[", StringComparison.Ordinal) ? line : "[stdout] " + line;
|
var normalized = line.StartsWith("[", StringComparison.Ordinal) ? line : "[stdout] " + line;
|
||||||
OnTaskMessage(_subscribedTaskId, normalized);
|
OnTaskMessage(_subscribedTaskId, normalized);
|
||||||
}
|
}
|
||||||
FlushClaudeBuffer();
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException) { throw; }
|
catch (OperationCanceledException) { throw; }
|
||||||
catch { /* best-effort replay */ }
|
catch { /* best-effort replay */ }
|
||||||
@@ -289,8 +284,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
FlushClaudeBuffer();
|
|
||||||
|
|
||||||
var kind = line.StartsWith("[sys]", StringComparison.OrdinalIgnoreCase) ? LogKind.Sys
|
var kind = line.StartsWith("[sys]", StringComparison.OrdinalIgnoreCase) ? LogKind.Sys
|
||||||
: line.StartsWith("[tool]", StringComparison.OrdinalIgnoreCase) ? LogKind.Tool
|
: line.StartsWith("[tool]", StringComparison.OrdinalIgnoreCase) ? LogKind.Tool
|
||||||
: line.StartsWith("[claude]", StringComparison.OrdinalIgnoreCase) ? LogKind.Claude
|
: line.StartsWith("[claude]", StringComparison.OrdinalIgnoreCase) ? LogKind.Claude
|
||||||
@@ -302,29 +295,9 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
|
|||||||
|
|
||||||
private void AppendStdoutLine(string line)
|
private void AppendStdoutLine(string line)
|
||||||
{
|
{
|
||||||
var formatted = _formatter.FormatLine(line);
|
if (_formatter.FormatLine(line) is not { } entries) return;
|
||||||
if (formatted is null) return;
|
foreach (var entry in LogLineViewModel.FromEntries(entries))
|
||||||
_claudeBuf.Append(formatted);
|
Log.Add(entry);
|
||||||
while (true)
|
|
||||||
{
|
|
||||||
var text = _claudeBuf.ToString();
|
|
||||||
var nl = text.IndexOf('\n');
|
|
||||||
if (nl < 0) break;
|
|
||||||
var piece = text[..nl].TrimEnd('\r');
|
|
||||||
if (!string.IsNullOrWhiteSpace(piece))
|
|
||||||
Log.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
|
|
||||||
_claudeBuf.Clear();
|
|
||||||
_claudeBuf.Append(text[(nl + 1)..]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void FlushClaudeBuffer()
|
|
||||||
{
|
|
||||||
if (_claudeBuf.Length == 0) return;
|
|
||||||
var piece = _claudeBuf.ToString().TrimEnd();
|
|
||||||
_claudeBuf.Clear();
|
|
||||||
if (!string.IsNullOrWhiteSpace(piece))
|
|
||||||
Log.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async System.Threading.Tasks.Task RefreshStatusAsync(string taskId)
|
private async System.Threading.Tasks.Task RefreshStatusAsync(string taskId)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using ClaudeDo.Ui.Helpers;
|
using ClaudeDo.Ui.Helpers;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Tests.Helpers;
|
namespace ClaudeDo.Ui.Tests.Helpers;
|
||||||
|
|
||||||
@@ -6,22 +7,15 @@ public class StreamLineFormatterTests
|
|||||||
{
|
{
|
||||||
private readonly StreamLineFormatter _formatter = new();
|
private readonly StreamLineFormatter _formatter = new();
|
||||||
|
|
||||||
|
private (LogKind Kind, string Text) Single(string line) => Assert.Single(_formatter.FormatLine(line)!);
|
||||||
|
|
||||||
// --- Assistant text blocks ---
|
// --- Assistant text blocks ---
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FormatLine_AssistantTextBlock_ReturnsTextContent()
|
public void FormatLine_AssistantTextBlock_ReturnsClaudeEntry()
|
||||||
{
|
{
|
||||||
var line = """{"type":"assistant","message":{"content":[{"type":"text","text":"Hello world"}]}}""";
|
var line = """{"type":"assistant","message":{"content":[{"type":"text","text":"Hello world"}]}}""";
|
||||||
Assert.Equal("Hello world\n", _formatter.FormatLine(line));
|
Assert.Equal((LogKind.Claude, "Hello world"), Single(line));
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public void FormatLine_AssistantConsecutiveTextBlocks_ReturnEachAppended()
|
|
||||||
{
|
|
||||||
var line1 = """{"type":"assistant","message":{"content":[{"type":"text","text":"Hello "}]}}""";
|
|
||||||
var line2 = """{"type":"assistant","message":{"content":[{"type":"text","text":"world"}]}}""";
|
|
||||||
Assert.Equal("Hello \n", _formatter.FormatLine(line1));
|
|
||||||
Assert.Equal("world\n", _formatter.FormatLine(line2));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -31,15 +25,60 @@ public class StreamLineFormatterTests
|
|||||||
Assert.Null(_formatter.FormatLine(line));
|
Assert.Null(_formatter.FormatLine(line));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Tool use, result, system, fallback ---
|
[Fact]
|
||||||
|
public void FormatLine_MixedBlocks_KeepOrderAndKinds()
|
||||||
|
{
|
||||||
|
var line = """{"type":"assistant","message":{"content":[{"type":"text","text":"Fixing it."},{"type":"tool_use","id":"x","name":"Edit","input":{"file_path":"C:/repo/src/Foo.cs"}}]}}""";
|
||||||
|
Assert.Equal(
|
||||||
|
[(LogKind.Claude, "Fixing it."), (LogKind.Edit, "[Edit] Foo.cs")],
|
||||||
|
_formatter.FormatLine(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tool use ---
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FormatLine_AssistantToolUseBlock_ReturnsToolNameLine()
|
public void FormatLine_AssistantToolUseBlock_ReturnsToolEntry()
|
||||||
{
|
{
|
||||||
var line = """{"type":"assistant","message":{"content":[{"type":"tool_use","id":"x","name":"Bash","input":{"command":"ls"}}]}}""";
|
var line = """{"type":"assistant","message":{"content":[{"type":"tool_use","id":"x","name":"Bash","input":{"command":"ls"}}]}}""";
|
||||||
Assert.Equal("[Bash] $ ls\n", _formatter.FormatLine(line));
|
Assert.Equal((LogKind.Tool, "[Bash] $ ls"), Single(line));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("Write")]
|
||||||
|
[InlineData("NotebookEdit")]
|
||||||
|
public void FormatLine_FileMutatingTools_AreEditKind(string tool)
|
||||||
|
{
|
||||||
|
var line = $$$"""{"type":"assistant","message":{"content":[{"type":"tool_use","id":"x","name":"{{{tool}}}","input":{"file_path":"a/b.txt"}}]}}""";
|
||||||
|
Assert.Equal((LogKind.Edit, $"[{tool}] b.txt"), Single(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FormatLine_LongBashCommand_IsNotCutAt120Chars()
|
||||||
|
{
|
||||||
|
var cmd = "cd \\\"C:/Private/.claudedo-worktrees/claude-do/385-unit-branch-teil-1-branch-naming-basis-b\\\" && dotnet build ClaudeDo.slnx -c Release --no-restore -v minimal";
|
||||||
|
var line = $$$"""{"type":"assistant","message":{"content":[{"type":"tool_use","id":"x","name":"Bash","input":{"command":"{{{cmd}}}"}}]}}""";
|
||||||
|
var (_, text) = Single(line);
|
||||||
|
Assert.EndsWith("-v minimal", text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Tool results ---
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FormatLine_ToolResult_IsToolResultKind()
|
||||||
|
{
|
||||||
|
var line = """{"type":"user","message":{"content":[{"type":"tool_result","content":"Build succeeded."}]}}""";
|
||||||
|
Assert.Equal((LogKind.ToolResult, "→ Build succeeded."), Single(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FormatLine_ToolResultError_IsStderrKind()
|
||||||
|
{
|
||||||
|
var line = """{"type":"user","message":{"content":[{"type":"tool_result","is_error":true,"content":"boom"}]}}""";
|
||||||
|
Assert.Equal((LogKind.Stderr, "→ error: boom"), Single(line));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Result, system, fallback ---
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FormatLine_InputJsonDelta_ReturnsNull()
|
public void FormatLine_InputJsonDelta_ReturnsNull()
|
||||||
{
|
{
|
||||||
@@ -48,17 +87,17 @@ public class StreamLineFormatterTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FormatLine_Result_ReturnsFormattedResult()
|
public void FormatLine_Result_ReturnsDoneEntry()
|
||||||
{
|
{
|
||||||
var line = """{"type":"result","result":"Done."}""";
|
var line = """{"type":"result","result":"Done."}""";
|
||||||
Assert.Equal("\n--- Result ---\nDone.\n", _formatter.FormatLine(line));
|
Assert.Equal((LogKind.Done, "--- Result ---\nDone."), Single(line));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FormatLine_ApiRetry_ReturnsRetryNotice()
|
public void FormatLine_ApiRetry_ReturnsSysEntry()
|
||||||
{
|
{
|
||||||
var line = """{"type":"system","subtype":"api_retry"}""";
|
var line = """{"type":"system","subtype":"api_retry"}""";
|
||||||
Assert.Equal("[Retrying API call...]\n", _formatter.FormatLine(line));
|
Assert.Equal((LogKind.Sys, "[Retrying API call...]"), Single(line));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -76,10 +115,9 @@ public class StreamLineFormatterTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FormatLine_MalformedJson_ReturnsRawLine()
|
public void FormatLine_MalformedJson_ReturnsRawStdoutEntry()
|
||||||
{
|
{
|
||||||
var line = "not json at all";
|
Assert.Equal((LogKind.Stdout, "not json at all"), Single("not json at all"));
|
||||||
Assert.Equal("not json at all", _formatter.FormatLine(line));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -91,5 +129,13 @@ public class StreamLineFormatterTests
|
|||||||
Assert.Null(_formatter.FormatLine(delta));
|
Assert.Null(_formatter.FormatLine(delta));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Trim ---
|
// --- Line splitting ---
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FromEntries_SplitsMultilineText_KeepingKind_AndDroppingBlanks()
|
||||||
|
{
|
||||||
|
var lines = LogLineViewModel.FromEntries([(LogKind.Done, "--- Result ---\n\nDone.\r\n")]).ToList();
|
||||||
|
Assert.Equal(["--- Result ---", "Done."], lines.Select(l => l.Text));
|
||||||
|
Assert.All(lines, l => Assert.Equal(LogKind.Done, l.Kind));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user