V13 / 7879e8b5740b
OllamaCliProgressTracker.cs
Lilith/Core/Ollama/OllamaCliProgressTracker.cs
Unchanged from the preceding versionCS
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
namespace Lilith.Core.Ollama;
/// <summary>
/// Reads <c>ollama pull</c> stdout/stderr and exposes the latest status for single-line logging.
/// </summary>
internal sealed class OllamaCliProgressTracker(string model)
{
private readonly object _sync = new();
private string _lastStatus = "starting…";
private int? _percent;
public string Model { get; } = model;
public void Update(string raw)
{
string line = StripAnsi(raw.Trim());
if (string.IsNullOrWhiteSpace(line))
{
return;
}
lock (_sync)
{
if (TryParsePullJson(line, out string? status, out int? percent))
{
_lastStatus = status ?? line;
if (percent.HasValue)
{
_percent = percent;
}
return;
}
_lastStatus = line;
}
}
public string GetDisplayMessage(TimeSpan elapsed, TimeSpan remaining)
{
lock (_sync)
{
if (_percent is { } pct)
{
return $"Pulling '{Model}': {_lastStatus} ({pct}%) — {elapsed:mm\\:ss} elapsed, {remaining:mm\\:ss} left";
}
return $"Pulling '{Model}': {_lastStatus} — {elapsed:mm\\:ss} elapsed, {remaining:mm\\:ss} left";
}
}
private static bool TryParsePullJson(string line, out string? status, out int? percent)
{
status = null;
percent = null;
if (!line.StartsWith('{'))
{
return false;
}
try
{
using JsonDocument doc = JsonDocument.Parse(line);
JsonElement root = doc.RootElement;
if (root.TryGetProperty("status", out JsonElement statusEl))
{
status = statusEl.GetString();
}
if (root.TryGetProperty("completed", out JsonElement doneEl) &&
root.TryGetProperty("total", out JsonElement totalEl) &&
totalEl.TryGetInt64(out long total) &&
total > 0 &&
doneEl.TryGetInt64(out long done))
{
percent = (int)(done * 100 / total);
}
return status is not null || percent.HasValue;
}
catch
{
return false;
}
}
private static string StripAnsi(string text) =>
RegexPatterns.AnsiEscape().Replace(text, string.Empty);
}