Forge
csharpdeeb25a2
1#nullable enable
2
3using CascadeIDE.Cockpit.DataBus;
4
5namespace CascadeIDE.Features.Agent.Environment;
6
7public enum VerifyRungUiState
8{
9 Pending,
10 Running,
11 Pass,
12 Fail,
13 Cancelled,
14 Died,
15 Skipped,
16}
17
18public sealed record VerifyRungUiEntry(
19 string RungId,
20 VerifyRungUiState State,
21 double DurationSeconds,
22 string? Detail);
23
24/// <summary>Shared Verify Epoch formatting for chat + PFD (agent-verify-epoch-view-v1).</summary>
25public static class AgentVerifyEpochFormatter
26{
27 public static readonly string[] OrderedRungs =
28 [
29 VerifyRung.DiagnoseFiles,
30 VerifyRung.CompileProject,
31 VerifyRung.BuildAffected,
32 VerifyRung.TestScoped,
33 VerifyRung.TestFull,
34 ];
35
36 public static string? MapTaskKindToRung(string kind) => kind switch
37 {
38 "roslyn.diagnose" => VerifyRung.DiagnoseFiles,
39 "msbuild.compile" => VerifyRung.BuildAffected,
40 "dotnet.test" => VerifyRung.TestScoped,
41 _ => null,
42 };
43
44 public static string FormatCompletedChatTrace(AgentRunCompleted evt)
45 {
46 var rungLines = FormatRungLinesFromCompleted(evt);
47 var status = evt.Green ? "green" : "failed";
48 return $"""
49 [AEE] verify {evt.RunId[..8]}…
50 {rungLines}
51 Status: {status} · max rung: {evt.MaxRungReached}
52 """;
53 }
54
55 public static string FormatRungLinesFromCompleted(AgentRunCompleted evt)
56 {
57 var entries = BuildEntriesFromTimeSlices(evt.TimeSlices, evt.Green, evt.MaxRungReached);
58 return FormatRungLineBlock(entries, includeSkipped: false);
59 }
60
61 public static IReadOnlyList<VerifyRungUiEntry> BuildEntriesFromTimeSlices(
62 IReadOnlyList<AgentTimeSlice> timeSlices,
63 bool green,
64 string maxRungReached)
65 {
66 var byRung = new Dictionary<string, VerifyRungUiEntry>(StringComparer.Ordinal);
67
68 foreach (var slice in timeSlices.Where(s => s.Phase == AgentRunPhaseKind.Environment))
69 {
70 var rung = TryExtractRungId(slice.Detail);
71 if (rung is null)
72 continue;
73
74 var state = ResolveStateFromDetail(slice.Detail, green && !IsFailureDetail(slice.Detail));
75 byRung[rung] = new VerifyRungUiEntry(rung, state, slice.DurationSeconds, slice.Detail);
76 }
77
78 var list = new List<VerifyRungUiEntry>();
79 foreach (var rung in OrderedRungs)
80 {
81 if (byRung.TryGetValue(rung, out var entry))
82 {
83 list.Add(entry);
84 continue;
85 }
86
87 if (ShouldMarkSkipped(rung, maxRungReached, green))
88 list.Add(new VerifyRungUiEntry(rung, VerifyRungUiState.Skipped, 0, null));
89 }
90
91 return list;
92 }
93
94 public static string FormatCompactLine(
95 bool isActive,
96 bool isStale,
97 bool displayGreen,
98 string? policy,
99 string? runId,
100 string? activeRungId,
101 double activeSeconds,
102 string? maxRungReached,
103 bool hostDied)
104 {
105 var policyText = string.IsNullOrWhiteSpace(policy) ? "standard" : policy;
106
107 if (isStale)
108 return $"⚠ verify устарел · {policyText}";
109
110 if (hostDied)
111 return $"☠ environment died · {policyText}";
112
113 if (isActive)
114 {
115 var rung = activeRungId ?? VerifyRung.DiagnoseFiles;
116 var elapsed = activeSeconds > 0 ? $"{activeSeconds:0.0}s" : "…";
117 var shortId = ShortRunId(runId);
118 return $"⟳ {rung} {elapsed} · {policyText} · {shortId}";
119 }
120
121 if (displayGreen)
122 return $"✓ {maxRungReached ?? VerifyRung.BuildAffected} · green ({policyText})";
123
124 if (!string.IsNullOrWhiteSpace(maxRungReached))
125 return $"✗ {maxRungReached} · failed ({policyText})";
126
127 return "";
128 }
129
130 public static string FormatExpandedBlock(
131 string? policy,
132 string? runId,
133 string? snapshotId,
134 bool isStale,
135 string? staleReason,
136 IReadOnlyList<VerifyRungUiEntry> rungs,
137 IReadOnlyList<AgentTimeSlice>? timeSlices,
138 bool displayGreen,
139 string? maxRungReached)
140 {
141 var lines = new List<string>
142 {
143 $"Verify · {policy ?? "standard"} · epoch {ShortRunId(runId)}",
144 $"snapshot {ShortSnapshot(snapshotId)}",
145 };
146
147 if (isStale)
148 lines.Add($"⚠ verify устарел — snapshot изменился ({staleReason ?? "stale"})");
149
150 lines.Add(FormatRungLineBlock(rungs, includeSkipped: true));
151
152 if (timeSlices is { Count: > 0 })
153 lines.Add(FormatTimeAccountingFooter(timeSlices, displayGreen, policy, maxRungReached));
154 else if (!isStale && !string.IsNullOrWhiteSpace(maxRungReached))
155 {
156 var status = displayGreen ? "green" : "failed";
157 lines.Add($"Status: {status} · max rung: {maxRungReached}");
158 }
159
160 return string.Join('\n', lines);
161 }
162
163 public static string FormatRungLineBlock(IReadOnlyList<VerifyRungUiEntry> entries, bool includeSkipped)
164 {
165 if (entries.Count == 0)
166 return " — (no rungs yet)";
167
168 var lines = new List<string>();
169 foreach (var entry in entries)
170 {
171 if (entry.State == VerifyRungUiState.Skipped && !includeSkipped)
172 continue;
173
174 var glyph = entry.State switch
175 {
176 VerifyRungUiState.Pass => "✓",
177 VerifyRungUiState.Running => "⟳",
178 VerifyRungUiState.Fail => "✗",
179 VerifyRungUiState.Cancelled => "⊘",
180 VerifyRungUiState.Died => "☠",
181 VerifyRungUiState.Skipped => "—",
182 _ => "·",
183 };
184
185 var duration = entry.State == VerifyRungUiState.Running
186 ? "running…"
187 : entry.DurationSeconds > 0
188 ? $"{entry.DurationSeconds:0.0}s"
189 : entry.State == VerifyRungUiState.Skipped
190 ? "(not required)"
191 : "—";
192
193 lines.Add($" {glyph} {entry.RungId,-18} {duration}");
194 }
195
196 return string.Join('\n', lines);
197 }
198
199 public static string FormatTimeAccountingFooter(
200 IReadOnlyList<AgentTimeSlice> slices,
201 bool displayGreen,
202 string? policy,
203 string? maxRungReached)
204 {
205 double Sum(AgentRunPhaseKind phase) =>
206 slices.Where(s => s.Phase == phase).Sum(s => s.DurationSeconds);
207
208 var reasoning = Sum(AgentRunPhaseKind.Reasoning);
209 var environment = Sum(AgentRunPhaseKind.Environment);
210 var blocked = Sum(AgentRunPhaseKind.Blocked);
211 var status = displayGreen ? "green" : "failed";
212 return $"Reasoning: {reasoning:0.0}s · Environment: {environment:0.0}s · Blocked: {blocked:0.0}s\nStatus: {status} ({policy ?? "standard"}) · max rung: {maxRungReached ?? ""}";
213 }
214
215 public static bool ShouldDisplayGreen(bool completedGreen, bool isStale, string maxRungReached) =>
216 completedGreen && !isStale && !string.IsNullOrWhiteSpace(maxRungReached);
217
218 internal static string? TryExtractRungId(string? detail)
219 {
220 if (string.IsNullOrWhiteSpace(detail))
221 return null;
222
223 foreach (var rung in OrderedRungs)
224 {
225 if (detail.StartsWith(rung, StringComparison.Ordinal)
226 || detail.Contains(rung, StringComparison.Ordinal))
227 return rung;
228 }
229
230 return null;
231 }
232
233 internal static bool IsFailureDetail(string? detail) =>
234 detail?.Contains("error", StringComparison.OrdinalIgnoreCase) == true
235 || detail?.Contains("fail", StringComparison.OrdinalIgnoreCase) == true
236 || detail?.Contains("missing", StringComparison.OrdinalIgnoreCase) == true
237 || detail?.Contains("cancelled", StringComparison.OrdinalIgnoreCase) == true;
238
239 private static VerifyRungUiState ResolveStateFromDetail(string? detail, bool passHint)
240 {
241 if (detail?.Contains("ci_parity marker", StringComparison.OrdinalIgnoreCase) == true)
242 return VerifyRungUiState.Pass;
243
244 if (detail?.Contains("cancelled", StringComparison.OrdinalIgnoreCase) == true)
245 return VerifyRungUiState.Cancelled;
246
247 if (detail?.Contains("died", StringComparison.OrdinalIgnoreCase) == true)
248 return VerifyRungUiState.Died;
249
250 if (IsFailureDetail(detail))
251 return VerifyRungUiState.Fail;
252
253 return passHint ? VerifyRungUiState.Pass : VerifyRungUiState.Running;
254 }
255
256 private static bool ShouldMarkSkipped(string rung, string maxRungReached, bool green)
257 {
258 var maxIndex = Array.IndexOf(OrderedRungs, maxRungReached);
259 var rungIndex = Array.IndexOf(OrderedRungs, rung);
260 if (maxIndex < 0 || rungIndex < 0)
261 return false;
262
263 return green && rungIndex > maxIndex;
264 }
265
266 private static string ShortRunId(string? runId) =>
267 string.IsNullOrWhiteSpace(runId) ? "—" : runId.Length <= 6 ? runId : runId[..6];
268
269 private static string ShortSnapshot(string? snapshotId)
270 {
271 if (string.IsNullOrWhiteSpace(snapshotId))
272 return "—";
273
274 return snapshotId.Length <= 12 ? snapshotId : snapshotId[..12] + "…";
275 }
276}
277
View only · write via MCP/CIDE