using System.Text; using System.Text.Json; namespace ClaudeDo.Ui.Helpers; public class StreamLineFormatter { private const int MaxLength = 50_000; private const int MaxArgChars = 120; public string? FormatLine(string line) { JsonDocument doc; try { doc = JsonDocument.Parse(line); } catch (JsonException) { return line; } using (doc) { var root = doc.RootElement; if (root.ValueKind != JsonValueKind.Object) return null; if (!root.TryGetProperty("type", out var typeProp)) return null; return typeProp.GetString() switch { "system" => FormatSystem(root), "assistant" => FormatAssistant(root), "user" => FormatUser(root), "result" => FormatResult(root), _ => null, }; } } private static string? FormatSystem(JsonElement root) { if (!root.TryGetProperty("subtype", out var subtypeProp)) return null; var subtype = subtypeProp.GetString(); switch (subtype) { case "api_retry": return "[Retrying API call...]\n"; case "init": { var sessionId = root.TryGetProperty("session_id", out var sid) ? sid.GetString() : null; var model = root.TryGetProperty("model", out var m) ? m.GetString() : null; var shortId = sessionId is { Length: >= 8 } ? sessionId[..8] : sessionId ?? "?"; var modelPart = string.IsNullOrEmpty(model) ? "" : $" · {model}"; return $"[session {shortId}{modelPart}]\n"; } default: return null; } } private static string? FormatAssistant(JsonElement root) { if (!TryGetContentArray(root, out var content)) return null; var sb = new StringBuilder(); foreach (var block in content.EnumerateArray()) { if (block.ValueKind != JsonValueKind.Object) continue; if (!block.TryGetProperty("type", out var blockTypeProp)) continue; switch (blockTypeProp.GetString()) { case "text": if (block.TryGetProperty("text", out var textProp)) { var text = textProp.GetString(); if (!string.IsNullOrEmpty(text)) { sb.Append(text); if (!text.EndsWith('\n')) sb.Append('\n'); } } break; case "tool_use": sb.Append(FormatToolUse(block)); sb.Append('\n'); break; case "thinking": default: // Filtered. break; } } return sb.Length == 0 ? null : sb.ToString(); } private static bool TryGetContentArray(JsonElement root, out JsonElement content) { content = default; if (!root.TryGetProperty("message", out var message)) return false; if (message.ValueKind != JsonValueKind.Object) return false; if (!message.TryGetProperty("content", out var c)) return false; if (c.ValueKind != JsonValueKind.Array) return false; content = c; return true; } private static string? FormatUser(JsonElement root) => null; private static string? FormatResult(JsonElement root) { if (root.TryGetProperty("result", out var resultProp)) return $"\n--- Result ---\n{resultProp.GetString()}\n"; return null; } private static string FormatToolUse(JsonElement block) { var name = block.TryGetProperty("name", out var nameProp) ? nameProp.GetString() ?? "?" : "?"; JsonElement input = default; var hasInput = block.TryGetProperty("input", out input) && input.ValueKind == JsonValueKind.Object; var label = name; if (hasInput && (name == "Task" || name == "Agent")) { var sub = GetStr(input, "subagent_type"); if (!string.IsNullOrEmpty(sub)) label = $"{name}: {sub}"; } string? arg = hasInput ? BuildToolArg(name, input) : null; return string.IsNullOrEmpty(arg) ? $"[{label}]" : $"[{label}] {arg}"; } private static string? BuildToolArg(string toolName, JsonElement input) { switch (toolName) { case "Read": case "Write": case "Edit": case "NotebookEdit": return Basename(GetStr(input, "file_path")); case "Bash": case "PowerShell": { var cmd = GetStr(input, "command"); return string.IsNullOrEmpty(cmd) ? null : "$ " + Truncate(cmd, MaxArgChars); } case "Grep": { var p = GetStr(input, "pattern"); return string.IsNullOrEmpty(p) ? null : $"\"{Truncate(p, MaxArgChars)}\""; } case "Glob": return Truncate(GetStr(input, "pattern"), MaxArgChars); case "Task": case "Agent": return Truncate(GetStr(input, "description"), MaxArgChars); case "WebFetch": return GetStr(input, "url"); case "WebSearch": { var q = GetStr(input, "query"); return string.IsNullOrEmpty(q) ? null : $"\"{Truncate(q, MaxArgChars)}\""; } case "TodoWrite": return null; default: return null; } } private static string? GetStr(JsonElement obj, string name) => obj.TryGetProperty(name, out var p) && p.ValueKind == JsonValueKind.String ? p.GetString() : null; private static string Basename(string? path) { if (string.IsNullOrEmpty(path)) return ""; var i = path.LastIndexOfAny(new[] { '/', '\\' }); return i < 0 ? path : path[(i + 1)..]; } private static string Truncate(string? s, int max) { if (string.IsNullOrEmpty(s)) return ""; return s.Length <= max ? s : s[..max] + "…"; } 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..]; } }