Parses Claude CLI stream-json output into human-readable text. Handles text deltas, tool use, results, API retries, and trims large files. All 13 tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
116 lines
3.4 KiB
C#
116 lines
3.4 KiB
C#
using System.Text;
|
|
using System.Text.Json;
|
|
|
|
namespace ClaudeDo.Ui.Helpers;
|
|
|
|
public class StreamLineFormatter
|
|
{
|
|
private const int MaxLength = 50_000;
|
|
|
|
public string? FormatLine(string line)
|
|
{
|
|
JsonDocument doc;
|
|
try
|
|
{
|
|
doc = JsonDocument.Parse(line);
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
return line;
|
|
}
|
|
|
|
using (doc)
|
|
{
|
|
var root = doc.RootElement;
|
|
if (!root.TryGetProperty("type", out var typeProp))
|
|
return null;
|
|
|
|
var type = typeProp.GetString();
|
|
|
|
switch (type)
|
|
{
|
|
case "stream_event":
|
|
return FormatStreamEvent(root);
|
|
|
|
case "result":
|
|
if (root.TryGetProperty("result", out var resultProp))
|
|
return $"\n--- Result ---\n{resultProp.GetString()}\n";
|
|
return null;
|
|
|
|
case "system":
|
|
if (root.TryGetProperty("subtype", out var subtypeProp) &&
|
|
subtypeProp.GetString() == "api_retry")
|
|
return "\n[Retrying API call...]\n";
|
|
return null;
|
|
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string? FormatStreamEvent(JsonElement root)
|
|
{
|
|
if (!root.TryGetProperty("event", out var ev))
|
|
return null;
|
|
if (!ev.TryGetProperty("type", out var evTypeProp))
|
|
return null;
|
|
|
|
var evType = evTypeProp.GetString();
|
|
|
|
switch (evType)
|
|
{
|
|
case "content_block_delta":
|
|
if (!ev.TryGetProperty("delta", out var delta))
|
|
return null;
|
|
if (!delta.TryGetProperty("type", out var deltaTypeProp))
|
|
return null;
|
|
var deltaType = deltaTypeProp.GetString();
|
|
if (deltaType == "text_delta")
|
|
{
|
|
return delta.TryGetProperty("text", out var textProp)
|
|
? textProp.GetString()
|
|
: null;
|
|
}
|
|
return null; // input_json_delta and others → skip
|
|
|
|
case "content_block_stop":
|
|
return "\n";
|
|
|
|
case "content_block_start":
|
|
if (!ev.TryGetProperty("content_block", out var cb))
|
|
return null;
|
|
if (cb.TryGetProperty("type", out var cbTypeProp) &&
|
|
cbTypeProp.GetString() == "tool_use" &&
|
|
cb.TryGetProperty("name", out var nameProp))
|
|
return $"\n[Tool: {nameProp.GetString()}]\n";
|
|
return null;
|
|
|
|
default:
|
|
return null; // message_start, message_delta, etc.
|
|
}
|
|
}
|
|
|
|
public string FormatFile(string filePath)
|
|
{
|
|
var sb = new StringBuilder();
|
|
foreach (var line in File.ReadLines(filePath))
|
|
{
|
|
var formatted = FormatLine(line);
|
|
if (formatted is not null)
|
|
sb.Append(formatted);
|
|
}
|
|
return Trim(sb.ToString());
|
|
}
|
|
|
|
public static string Trim(string text)
|
|
{
|
|
if (text.Length <= MaxLength) return text;
|
|
var trimStart = text.Length - MaxLength;
|
|
var newlineAfter = text.IndexOf('\n', trimStart);
|
|
if (newlineAfter >= 0 && newlineAfter < trimStart + 200)
|
|
trimStart = newlineAfter + 1;
|
|
return text[trimStart..];
|
|
}
|
|
}
|