using System.Text;using System.Text.Json;using System.Text.RegularExpressions;namespace Agent.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) => Regex.Replace(text, @"\x1b\[[0-9;]*[a-zA-Z]", string.Empty);}internal static class OllamaCliStreamReader{ public static async Task PumpAsync(TextReader reader, OllamaCliProgressTracker tracker, CancellationToken cancellationToken = default) { var buffer = new char[512]; var segment = new StringBuilder(); while (!cancellationToken.IsCancellationRequested) { int read = await reader.ReadAsync(buffer, cancellationToken); if (read <= 0) { break; } for (int i = 0; i < read; i++) { char c = buffer[i]; if (c is '\r' or '\n') { Flush(segment, tracker); } else { segment.Append(c); } } } Flush(segment, tracker); } private static void Flush(StringBuilder segment, OllamaCliProgressTracker tracker) { if (segment.Length == 0) { return; } tracker.Update(segment.ToString()); segment.Clear(); }}
Documentation
OllamaCliProgress
Console progress reporting during model pulls and long operations.
OllamaCliProgress.cs
- Formats download / wait status for terminal output while Ollama starts or pulls models.