Forge
csharpdeeb25a2
1using CascadeIDE.Cockpit.DataBus;
2
3namespace CascadeIDE.Features.Agent.Environment;
4
5/// <summary>Live Verify Epoch state for PFD instrument + expand panel (ADR 0148 W3).</summary>
6public sealed class AgentVerifyEpochInstrument
7{
8 private readonly object _gate = new();
9 private readonly Dictionary<string, VerifyRungUiEntry> _rungs = new(StringComparer.Ordinal);
10
11 private string? _runId;
12 private string? _policy;
13 private string? _snapshotId;
14 private string? _maxRungReached;
15 private string? _staleReason;
16 private string? _activeTaskKind;
17 private DateTimeOffset _activeSinceUtc;
18 private bool _isActive;
19 private bool _isStale;
20 private bool _hostDied;
21 private bool _completedGreen;
22 private bool _frozen;
23 private List<AgentTimeSlice> _timeSlices = [];
24
25 public event Action? Changed;
26
27 public bool IsVisible { get; private set; }
28
29 public bool IsActive
30 {
31 get { lock (_gate) return _isActive; }
32 }
33
34 public bool IsStale
35 {
36 get { lock (_gate) return _isStale; }
37 }
38
39 public bool IsCaution
40 {
41 get { lock (_gate) return _isStale || _hostDied || (!_completedGreen && _frozen); }
42 }
43
44 public bool ShowCancel
45 {
46 get { lock (_gate) return _isActive && !_isStale; }
47 }
48
49 public bool ShowRetry
50 {
51 get { lock (_gate) return _hostDied || (_isStale && !_isActive); }
52 }
53
54 public bool DisplayGreen
55 {
56 get { lock (_gate) return AgentVerifyEpochFormatter.ShouldDisplayGreen(_completedGreen, _isStale, _maxRungReached ?? ""); }
57 }
58
59 public string CompactLine => Snapshot().CompactLine;
60
61 public string ExpandedText => Snapshot().ExpandedText;
62
63 public AgentVerifyEpochUiSnapshot Snapshot()
64 {
65 lock (_gate)
66 {
67 var rungList = BuildRungListLocked();
68 var activeRung = ResolveActiveRungLocked();
69 var activeSeconds = _isActive
70 ? (DateTimeOffset.UtcNow - _activeSinceUtc).TotalSeconds
71 : 0;
72
73 return new AgentVerifyEpochUiSnapshot(
74 IsVisible,
75 CompactLine: AgentVerifyEpochFormatter.FormatCompactLine(
76 _isActive,
77 _isStale,
78 DisplayGreenLocked(),
79 _policy,
80 _runId,
81 activeRung,
82 activeSeconds,
83 _maxRungReached,
84 _hostDied),
85 ExpandedText: AgentVerifyEpochFormatter.FormatExpandedBlock(
86 _policy,
87 _runId,
88 _snapshotId,
89 _isStale,
90 _staleReason,
91 rungList,
92 _timeSlices,
93 DisplayGreenLocked(),
94 _maxRungReached),
95 IsCaution,
96 ShowCancel: ShowCancel,
97 ShowRetry: ShowRetry,
98 DisplayGreen: DisplayGreenLocked());
99 }
100 }
101
102 public void Reset()
103 {
104 lock (_gate)
105 {
106 _rungs.Clear();
107 _runId = null;
108 _policy = null;
109 _snapshotId = null;
110 _maxRungReached = null;
111 _staleReason = null;
112 _activeTaskKind = null;
113 _isActive = false;
114 _isStale = false;
115 _hostDied = false;
116 _completedGreen = false;
117 _frozen = false;
118 _timeSlices = [];
119 IsVisible = false;
120 }
121
122 Changed?.Invoke();
123 }
124
125 public void OnRunStarted(AgentRunStarted evt)
126 {
127 lock (_gate)
128 {
129 _rungs.Clear();
130 foreach (var rung in AgentVerifyEpochFormatter.OrderedRungs)
131 _rungs[rung] = new VerifyRungUiEntry(rung, VerifyRungUiState.Pending, 0, null);
132
133 _runId = evt.RunId;
134 _policy = evt.VerifyPolicy;
135 _snapshotId = evt.VerifySnapshotId;
136 _maxRungReached = null;
137 _staleReason = null;
138 _activeTaskKind = VerifyRung.DiagnoseFiles;
139 _activeSinceUtc = DateTimeOffset.UtcNow;
140 _isActive = true;
141 _isStale = false;
142 _hostDied = false;
143 _completedGreen = false;
144 _frozen = false;
145 _timeSlices = [];
146 IsVisible = true;
147
148 SetRungLocked(VerifyRung.DiagnoseFiles, VerifyRungUiState.Running, 0, null);
149 }
150
151 Changed?.Invoke();
152 }
153
154 public void OnTaskChanged(AgentEnvironmentTaskChanged evt)
155 {
156 if (evt.State is not (AgentEnvironmentTaskState.Running or AgentEnvironmentTaskState.Queued))
157 return;
158
159 lock (_gate)
160 {
161 if (!_isActive || !string.Equals(_runId, evt.RunId, StringComparison.Ordinal))
162 return;
163
164 _activeTaskKind = evt.Kind;
165 _activeSinceUtc = DateTimeOffset.UtcNow;
166 var rung = AgentVerifyEpochFormatter.MapTaskKindToRung(evt.Kind);
167 if (rung is not null)
168 SetRungLocked(rung, VerifyRungUiState.Running, 0, evt.ProgressMessage);
169 }
170
171 Changed?.Invoke();
172 }
173
174 public void OnTaskCompleted(AgentEnvironmentTaskCompleted evt)
175 {
176 lock (_gate)
177 {
178 if (!string.Equals(_runId, evt.RunId, StringComparison.Ordinal))
179 return;
180
181 var rung = AgentVerifyEpochFormatter.MapTaskKindToRung(evt.Kind);
182 if (rung is null)
183 return;
184
185 var failed = evt.ResultSummary.Contains("fail", StringComparison.OrdinalIgnoreCase)
186 || evt.ResultSummary.Contains("error", StringComparison.OrdinalIgnoreCase);
187 var state = failed ? VerifyRungUiState.Fail : VerifyRungUiState.Pass;
188 SetRungLocked(
189 rung,
190 state,
191 evt.DurationMs / 1000.0,
192 evt.ResultSummary);
193 _maxRungReached = rung;
194 }
195
196 Changed?.Invoke();
197 }
198
199 public void OnTaskDied(AgentEnvironmentTaskDied evt)
200 {
201 lock (_gate)
202 {
203 if (!string.Equals(_runId, evt.RunId, StringComparison.Ordinal))
204 return;
205
206 _hostDied = true;
207 var rung = AgentVerifyEpochFormatter.MapTaskKindToRung(_activeTaskKind ?? "")
208 ?? VerifyRung.BuildAffected;
209 SetRungLocked(rung, VerifyRungUiState.Died, 0, evt.StderrTail);
210 }
211
212 Changed?.Invoke();
213 }
214
215 public void OnRunCompleted(AgentRunCompleted evt)
216 {
217 lock (_gate)
218 {
219 if (!string.Equals(_runId, evt.RunId, StringComparison.Ordinal))
220 return;
221
222 _isActive = false;
223 _frozen = true;
224 _completedGreen = evt.Green;
225 _maxRungReached = evt.MaxRungReached;
226 _timeSlices = evt.TimeSlices.ToList();
227
228 foreach (var entry in AgentVerifyEpochFormatter.BuildEntriesFromTimeSlices(
229 evt.TimeSlices,
230 evt.Green,
231 evt.MaxRungReached))
232 {
233 _rungs[entry.RungId] = entry;
234 }
235
236 IsVisible = true;
237 }
238
239 Changed?.Invoke();
240 }
241
242 public void OnEpochStale(AgentVerifyEpochStale evt)
243 {
244 lock (_gate)
245 {
246 if (!string.Equals(_runId, evt.RunId, StringComparison.Ordinal))
247 return;
248
249 _isStale = true;
250 _staleReason = evt.Reason;
251 IsVisible = true;
252 }
253
254 Changed?.Invoke();
255 }
256
257 public void HideAfterIdle()
258 {
259 lock (_gate)
260 {
261 if (_isActive || _isStale)
262 return;
263
264 IsVisible = false;
265 }
266
267 Changed?.Invoke();
268 }
269
270 private bool DisplayGreenLocked() =>
271 AgentVerifyEpochFormatter.ShouldDisplayGreen(_completedGreen, _isStale, _maxRungReached ?? "");
272
273 private string? ResolveActiveRungLocked()
274 {
275 if (!_isActive)
276 return _maxRungReached;
277
278 var running = _rungs.Values.FirstOrDefault(r => r.State == VerifyRungUiState.Running);
279 if (running is not null)
280 return running.RungId;
281
282 return AgentVerifyEpochFormatter.MapTaskKindToRung(_activeTaskKind ?? "")
283 ?? VerifyRung.DiagnoseFiles;
284 }
285
286 private IReadOnlyList<VerifyRungUiEntry> BuildRungListLocked()
287 {
288 if (_frozen && _timeSlices.Count > 0)
289 return AgentVerifyEpochFormatter.BuildEntriesFromTimeSlices(_timeSlices, _completedGreen, _maxRungReached ?? "");
290
291 return AgentVerifyEpochFormatter.OrderedRungs
292 .Select(rung => _rungs.TryGetValue(rung, out var entry)
293 ? entry
294 : new VerifyRungUiEntry(rung, VerifyRungUiState.Pending, 0, null))
295 .ToList();
296 }
297
298 private void SetRungLocked(string rungId, VerifyRungUiState state, double durationSeconds, string? detail)
299 {
300 _rungs[rungId] = new VerifyRungUiEntry(rungId, state, durationSeconds, detail);
301 }
302}
303
304public sealed record AgentVerifyEpochUiSnapshot(
305 bool IsVisible,
306 string CompactLine,
307 string ExpandedText,
308 bool IsCaution,
309 bool ShowCancel,
310 bool ShowRetry,
311 bool DisplayGreen);
312
View only · write via MCP/CIDE