← All source files

V9 / fa079a940f8f

OllamaCliProgressTracker.cs

Lilith/Core/Ollama/OllamaCliProgressTracker.cs

Unchanged from the preceding version
CS
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);
}