namespace AxCopilot.Services; /// /// 실행 중/최근 task 상태를 관리하는 전용 저장소. /// AppStateService와 UI가 같은 로직을 공유할 수 있게 분리합니다. /// public sealed class TaskRunStore { public sealed class TaskRun { public string Id { get; init; } = ""; public string Kind { get; set; } = ""; public string Title { get; set; } = ""; public string Status { get; set; } = "running"; public string Summary { get; set; } = ""; public DateTime StartedAt { get; set; } = DateTime.Now; public DateTime UpdatedAt { get; set; } = DateTime.Now; public string? FilePath { get; set; } } private readonly List _active = new(); private readonly List _recent = new(); public IReadOnlyList ActiveTasks => _active; public IReadOnlyList RecentTasks => _recent; public event Action? Changed; public void Upsert(string id, string kind, string title, string summary, string status = "running", string? filePath = null) { var existing = _active.FirstOrDefault(t => string.Equals(t.Id, id, StringComparison.OrdinalIgnoreCase)); if (existing == null) { _active.Add(new TaskRun { Id = id, Kind = kind, Title = title, Summary = summary, Status = status, FilePath = filePath, StartedAt = DateTime.Now, UpdatedAt = DateTime.Now, }); Changed?.Invoke(); return; } existing.Kind = kind; existing.Title = title; existing.Summary = summary; existing.Status = status; existing.FilePath = filePath; existing.UpdatedAt = DateTime.Now; Changed?.Invoke(); } public void Complete(string id, string? summary = null, string status = "completed") { var existing = _active.FirstOrDefault(t => string.Equals(t.Id, id, StringComparison.OrdinalIgnoreCase)); if (existing == null) return; _active.Remove(existing); existing.Status = status; if (!string.IsNullOrWhiteSpace(summary)) existing.Summary = summary; existing.UpdatedAt = DateTime.Now; _recent.Insert(0, existing); TrimRecent(); Changed?.Invoke(); } public void CompleteByPrefix(string prefix, string? summary = null, string status = "completed") { var matches = _active .Where(t => t.Id.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) .ToList(); foreach (var match in matches) { _active.Remove(match); match.Status = status; if (!string.IsNullOrWhiteSpace(summary)) match.Summary = summary; match.UpdatedAt = DateTime.Now; _recent.Insert(0, match); } if (matches.Count == 0) return; TrimRecent(); Changed?.Invoke(); } public void RestoreRecent(IEnumerable? recent) { RestoreState(null, recent); } public void RestoreState(IEnumerable? active, IEnumerable? recent) { _active.Clear(); _recent.Clear(); if (active != null) { _active.AddRange(active .OrderByDescending(t => t.UpdatedAt) .Take(25)); } if (recent != null) { _recent.AddRange(recent .OrderByDescending(t => t.UpdatedAt) .Take(25)); } Changed?.Invoke(); } private void TrimRecent() { if (_recent.Count > 25) _recent.RemoveRange(25, _recent.Count - 25); } }