Forge
csharpdeeb25a2
1using CascadeIDE.Cockpit.DataBus;
2
3namespace CascadeIDE.Features.Agent.Environment;
4
5/// <summary>Отслеживание idle_user (ADR 0148 §8.1.4): потеря фокуса CIDE ≥ порога.</summary>
6public sealed class AgentIdleUserTracker
7{
8 private readonly object _gate = new();
9 private DateTimeOffset? _focusLostUtc;
10 private bool _cideFocused = true;
11 private readonly List<AgentTimeSlice> _slices = [];
12
13 public void NotifyCideFocus(bool focused)
14 {
15 lock (_gate)
16 {
17 if (focused)
18 {
19 FlushIdleSliceLocked();
20 _cideFocused = true;
21 _focusLostUtc = null;
22 return;
23 }
24
25 if (_cideFocused)
26 _focusLostUtc = DateTimeOffset.UtcNow;
27 _cideFocused = false;
28 }
29 }
30
31 public void SampleWhileUnfocused(int thresholdMs)
32 {
33 if (thresholdMs <= 0)
34 return;
35
36 lock (_gate)
37 {
38 if (_cideFocused || _focusLostUtc is null)
39 return;
40
41 var elapsed = (DateTimeOffset.UtcNow - _focusLostUtc.Value).TotalMilliseconds;
42 if (elapsed < thresholdMs)
43 return;
44
45 _slices.Add(new AgentTimeSlice(
46 AgentRunPhaseKind.IdleUser,
47 elapsed / 1000.0,
48 $"idle_user ≥ {thresholdMs}ms"));
49 _focusLostUtc = DateTimeOffset.UtcNow;
50 }
51 }
52
53 public IReadOnlyList<AgentTimeSlice> DrainSlices()
54 {
55 lock (_gate)
56 {
57 FlushIdleSliceLocked();
58 if (_slices.Count == 0)
59 return [];
60
61 var copy = _slices.ToArray();
62 _slices.Clear();
63 return copy;
64 }
65 }
66
67 private void FlushIdleSliceLocked()
68 {
69 if (_focusLostUtc is null || _cideFocused)
70 return;
71
72 var elapsed = (DateTimeOffset.UtcNow - _focusLostUtc.Value).TotalSeconds;
73 if (elapsed > 0.05)
74 _slices.Add(new AgentTimeSlice(AgentRunPhaseKind.IdleUser, elapsed, "idle_user (focus returned)"));
75 _focusLostUtc = null;
76 }
77}
78
View only · write via MCP/CIDE