feat(ui): colour agent output by kind and stop cutting tool calls at 120 chars

StreamLineFormatter now returns kind-tagged entries (text, tool, edit,
tool result, error, sys, result) instead of a flat string, so the
existing LogKind colours finally apply to stream-json output. Tool
argument/result limit raised from 120 to 500 chars.
This commit is contained in:
mika kuns
2026-10-01 10:49:52 +02:00
parent 0e4de9e4c0
commit 9bb0e67a97
6 changed files with 149 additions and 136 deletions
@@ -1,6 +1,6 @@
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
{
@@ -9,26 +9,42 @@ public sealed class LogLineViewModel
public string TimestampFormatted { get; } = DateTime.Now.ToString("HH:mm:ss");
public string KindMarker => Kind switch
{
LogKind.Sys => "sys",
LogKind.Tool => "tool",
LogKind.Claude => "claude",
LogKind.Stdout => "out",
LogKind.Stderr => "err",
LogKind.Done => "done",
LogKind.Msg => "claude",
LogKind.User => "you",
_ => "",
LogKind.Sys => "sys",
LogKind.Tool => "tool",
LogKind.Claude => "claude",
LogKind.Stdout => "out",
LogKind.Stderr => "err",
LogKind.Done => "done",
LogKind.Msg => "claude",
LogKind.User => "you",
LogKind.Edit => "edit",
LogKind.ToolResult => "result",
_ => "",
};
public string ClassName => Kind switch
{
LogKind.Sys => "log-sys",
LogKind.Tool => "log-tool",
LogKind.Claude => "log-claude",
LogKind.Stdout => "log-stdout",
LogKind.Stderr => "log-stderr",
LogKind.Done => "log-done",
LogKind.Msg => "log-msg",
LogKind.User => "log-user",
_ => "",
LogKind.Sys => "log-sys",
LogKind.Tool => "log-tool",
LogKind.Claude => "log-claude",
LogKind.Stdout => "log-stdout",
LogKind.Stderr => "log-stderr",
LogKind.Done => "log-done",
LogKind.Msg => "log-msg",
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.Text;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Ui.Helpers;
@@ -13,7 +12,6 @@ public sealed partial class PrepPanelViewModel : ViewModelBase, IDisposable
{
private readonly IWorkerClient _worker;
private readonly StreamLineFormatter _formatter = new();
private readonly StringBuilder _prepClaudeBuf = new();
private readonly Action _onPrepStartedHandler;
private readonly Action<string> _onPrepLineHandler;
@@ -128,24 +126,8 @@ public sealed partial class PrepPanelViewModel : ViewModelBase, IDisposable
private void AppendStdoutLine(string line)
{
var formatted = _formatter.FormatLine(line);
if (formatted is null) return;
AppendClaudeText(formatted);
}
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)..]);
}
if (_formatter.FormatLine(line) is not { } entries) return;
foreach (var entry in LogLineViewModel.FromEntries(entries))
PrepLog.Add(entry);
}
}
@@ -1,5 +1,4 @@
using System.Collections.ObjectModel;
using System.Text;
using System.Text.Json;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -18,7 +17,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
private readonly IWorkerClient _worker;
private readonly StreamLineFormatter _formatter = new();
private readonly StringBuilder _claudeBuf = new();
private string? _subscribedTaskId;
public string? SubscribedTaskId => _subscribedTaskId;
@@ -105,7 +103,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
_onTaskFinished = (slot, taskId, status, finishedAt) =>
{
if (taskId != _subscribedTaskId) return;
FlushClaudeBuffer();
Log.Add(new LogLineViewModel
{
Kind = LogKind.Done,
@@ -182,7 +179,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
public void Reset()
{
Log.Clear();
_claudeBuf.Clear();
_subscribedTaskId = null;
AgentState = "idle";
SessionOutcome = null;
@@ -272,7 +268,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
var normalized = line.StartsWith("[", StringComparison.Ordinal) ? line : "[stdout] " + line;
OnTaskMessage(_subscribedTaskId, normalized);
}
FlushClaudeBuffer();
}
catch (OperationCanceledException) { throw; }
catch { /* best-effort replay */ }
@@ -289,8 +284,6 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
return;
}
FlushClaudeBuffer();
var kind = line.StartsWith("[sys]", StringComparison.OrdinalIgnoreCase) ? LogKind.Sys
: line.StartsWith("[tool]", StringComparison.OrdinalIgnoreCase) ? LogKind.Tool
: line.StartsWith("[claude]", StringComparison.OrdinalIgnoreCase) ? LogKind.Claude
@@ -302,29 +295,9 @@ public sealed partial class TaskMonitorViewModel : ViewModelBase, IDisposable
private void AppendStdoutLine(string line)
{
var formatted = _formatter.FormatLine(line);
if (formatted is null) return;
_claudeBuf.Append(formatted);
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 });
if (_formatter.FormatLine(line) is not { } entries) return;
foreach (var entry in LogLineViewModel.FromEntries(entries))
Log.Add(entry);
}
private async System.Threading.Tasks.Task RefreshStatusAsync(string taskId)