Skip to content

Commit b598155

Browse files
committed
fixes for icloud and auto sleep control
1 parent 8daa47c commit b598155

9 files changed

Lines changed: 144 additions & 206 deletions

File tree

DoomCoder/AgentTracking/AgentTrackingManager.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,18 @@ final class AgentTrackingManager {
148148
private(set) var sessions: [String: Session] = [:]
149149
var liveSessions: [Session] { sessions.values.filter(\.isLive).sorted { $0.updatedAt > $1.updatedAt } }
150150

151+
/// Local wall-clock timestamp of the most recent hook per agent — the
152+
/// auto-sleep truth source. Immune to remote clock skew.
153+
private(set) var lastHookByAgent: [TrackedAgent: Date] = [:]
154+
/// Most recent hook from ANY agent — single signal for the global 10-min timer.
155+
private(set) var lastAnyHookAt: Date = .distantPast
156+
157+
/// Unique agent types with a hook received in the last 10 minutes.
158+
var hookFreshAgents: [TrackedAgent] {
159+
let cutoff = Date().addingTimeInterval(-600)
160+
return lastHookByAgent.compactMap { agent, t in t > cutoff ? agent : nil }
161+
}
162+
151163
/// Single source of truth for an agent's effective display state, used by
152164
/// the panel UI (TrackAgentsView/TrackAccordion) AND the Mac→iOS publisher
153165
/// so both always agree.
@@ -209,8 +221,8 @@ final class AgentTrackingManager {
209221
private func sweepEvictedSessions() {
210222
var changed = false
211223
let now = Date()
212-
// Matches SleepManager.autoIdleWindowSeconds — IDE hooks older than this
213-
// are treated as idle. Keep in sync if that constant changes.
224+
// Matches the 10-min hook window used by SleepManager — IDE hooks older
225+
// than this are treated as idle.
214226
let ideIdleWindow: TimeInterval = 600
215227
// Grace before finalizing a live CLI session that never reported a pid:
216228
// gives a freshly-started session time to emit its first pid-bearing hook.
@@ -375,7 +387,10 @@ final class AgentTrackingManager {
375387
let hadPermission = s.awaitingPermission
376388
s.apply(normalized)
377389
// Receipt-time stamp: the real "a hook just fired" signal for Auto mode.
378-
s.lastHookReceivedAt = Date()
390+
let hookReceivedAt = Date()
391+
s.lastHookReceivedAt = hookReceivedAt
392+
lastHookByAgent[normalized.agent] = hookReceivedAt
393+
lastAnyHookAt = hookReceivedAt
379394
// Track the latest reported pid (CLI agents only — used by Auto mode
380395
// liveness checks). Keep the last known pid if this event lacks one.
381396
if env.pid > 0 { s.pid = pid_t(env.pid) }

DoomCoder/AgentTracking/CloudKitPusher.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,7 @@ final class CloudKitPusher {
346346
lastAppliedAt: ud.object(forKey: Self.lastAppliedAtKey) as? Date,
347347
masterEnabled: ud.object(forKey: CloudKitPusherDelegate.masterEnabledKey) as? Bool ?? true,
348348
agentStatusJSON: agentJSON,
349-
autoGraceEndsAt: sm.autoGraceEndsAt
349+
autoGraceEndsAt: nil
350350
)
351351
engine.state.add(pendingRecordZoneChanges: [.saveRecord(rec.recordID)])
352352
pendingMacStatus = rec

DoomCoder/AgentTracking/TrackAgentsPopover.swift

Lines changed: 10 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ struct TrackAgentsView: View {
7272

7373
@ViewBuilder
7474
private func row(_ agent: TrackedAgent) -> some View {
75-
let live = manager.liveSessions.first { $0.agent == agent }
7675
let isInstalled = installed[agent] ?? false
7776
let isOn = enabled[agent] ?? true
7877
let state = manager.effectiveState(for: agent)
@@ -98,7 +97,7 @@ struct TrackAgentsView: View {
9897
.frame(width: 7, height: 7)
9998
.symbolEffect(.pulse, isActive: state == .running)
10099
.accessibilityHidden(true)
101-
Text(subtitle(agent: agent, live: live))
100+
Text(subtitle(agent: agent))
102101
.font(.caption)
103102
.foregroundStyle(.secondary)
104103
.contentTransition(.interpolate)
@@ -144,17 +143,10 @@ struct TrackAgentsView: View {
144143

145144
// MARK: - Helpers
146145

147-
private func subtitle(agent: TrackedAgent, live: AgentTrackingManager.Session?) -> String {
148-
if let live {
149-
let state = live.displayState
150-
if state == .completed || state == .open {
151-
let elapsed = Int(Date().timeIntervalSince(live.updatedAt))
152-
let ago = elapsed < 60 ? "just now"
153-
: elapsed < 3600 ? "\(elapsed / 60)m ago"
154-
: "\(elapsed / 3600)h ago"
155-
return state == .completed ? "Completed · \(ago)" : "Idle · \(ago)"
156-
}
157-
return live.status
146+
private func subtitle(agent: TrackedAgent) -> String {
147+
if let t = AgentTrackingManager.shared.lastHookByAgent[agent],
148+
t.timeIntervalSinceNow > -600 {
149+
return "running"
158150
}
159151
let monitor = AgentTrackingManager.shared.processMonitor
160152
if agent.isIDEAgent, monitor.isAppRunning[agent] == true {
@@ -232,7 +224,6 @@ struct TrackAccordion: View {
232224

233225
@ViewBuilder
234226
private func compactRow(_ agent: TrackedAgent) -> some View {
235-
let live = manager.liveSessions.first { $0.agent == agent }
236227
let state = manager.effectiveState(for: agent)
237228
HStack(alignment: .center, spacing: 10) {
238229
AgentIconView(agent: agent, size: 20)
@@ -243,7 +234,7 @@ struct TrackAccordion: View {
243234
Circle().fill(stateColor(state)).frame(width: 6, height: 6)
244235
.symbolEffect(.pulse, isActive: state == .running)
245236
.accessibilityHidden(true)
246-
Text(subtitle(agent: agent, live: live))
237+
Text(subtitle(agent: agent))
247238
.font(.caption2).foregroundStyle(.secondary)
248239
.contentTransition(.interpolate)
249240
}
@@ -278,17 +269,10 @@ struct TrackAccordion: View {
278269
.transition(.opacity.combined(with: .offset(y: -8)))
279270
}
280271

281-
private func subtitle(agent: TrackedAgent, live: AgentTrackingManager.Session?) -> String {
282-
if let live {
283-
let state = live.displayState
284-
if state == .completed || state == .open {
285-
let elapsed = Int(Date().timeIntervalSince(live.updatedAt))
286-
let ago = elapsed < 60 ? "just now"
287-
: elapsed < 3600 ? "\(elapsed / 60)m ago"
288-
: "\(elapsed / 3600)h ago"
289-
return state == .completed ? "Completed · \(ago)" : "Idle · \(ago)"
290-
}
291-
return live.status
272+
private func subtitle(agent: TrackedAgent) -> String {
273+
if let t = AgentTrackingManager.shared.lastHookByAgent[agent],
274+
t.timeIntervalSinceNow > -600 {
275+
return "running"
292276
}
293277
let monitor = AgentTrackingManager.shared.processMonitor
294278
if agent.isIDEAgent, monitor.isAppRunning[agent] == true {

DoomCoder/PanelRootView.swift

Lines changed: 13 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -202,15 +202,12 @@ struct PanelRootView: View {
202202

203203
private var masterSubtitle: String {
204204
if !masterEnabled { return "Suspended — nothing is active" }
205-
if sleepManager.isActive, !tracking.liveSessions.isEmpty {
206-
let n = tracking.liveSessions.count
207-
return "Awake · \(n) agent\(n == 1 ? "" : "s") live"
205+
let n = tracking.hookFreshAgents.count
206+
if sleepManager.isActive, n > 0 {
207+
return "Awake · \(n) agent\(n == 1 ? "" : "s") working"
208208
}
209209
if sleepManager.isActive { return "Active · Mac awake" }
210-
if !tracking.liveSessions.isEmpty {
211-
let n = tracking.liveSessions.count
212-
return "\(n) agent\(n == 1 ? "" : "s") live"
213-
}
210+
if n > 0 { return "\(n) agent\(n == 1 ? "" : "s") working" }
214211
return "Ready"
215212
}
216213

@@ -310,13 +307,9 @@ struct PanelRootView: View {
310307
.disclosureGroupStyle(PillDisclosureStyle())
311308
}
312309
.transition(.opacity.combined(with: .scale(scale: 0.96)))
313-
} else if let graceEnd = sleepManager.autoGraceEndsAt, graceEnd > Date() {
314-
// Agents finished: the single visible countdown before sleeping.
315-
gracePill(endsAt: graceEnd)
316-
.transition(.opacity.combined(with: .scale(scale: 0.96)))
317310
} else {
318-
statusPill(icon: "powersleep",
319-
text: "Idle · sleeps when agents finish",
311+
statusPill(icon: "moon",
312+
text: "Delegated · macOS controls sleep",
320313
tint: .secondary)
321314
.transition(.opacity.combined(with: .scale(scale: 0.96)))
322315
}
@@ -391,8 +384,7 @@ struct PanelRootView: View {
391384
case .auto:
392385
let n = sleepManager.activeAgentCount
393386
if n > 0 { return "Auto · \(n) agent\(n == 1 ? "" : "s") working" }
394-
if sleepManager.autoGraceEndsAt != nil { return "Auto · releasing soon" }
395-
return "Auto · sleeps when idle"
387+
return "Auto · macOS controls sleep"
396388
}
397389
}
398390

@@ -403,20 +395,20 @@ struct PanelRootView: View {
403395
VStack(spacing: 10) {
404396
HStack(spacing: 10) {
405397
iconChip(system: "antenna.radiowaves.left.and.right",
406-
active: !tracking.liveSessions.isEmpty,
398+
active: !tracking.hookFreshAgents.isEmpty,
407399
activeTint: .green)
408400
VStack(alignment: .leading, spacing: 1) {
409401
Text("Agent Tracking")
410402
.font(.system(size: 13, weight: .medium))
411-
Text(tracking.liveSessions.isEmpty
412-
? "No sessions running"
413-
: "Listening for events")
403+
Text(tracking.hookFreshAgents.isEmpty
404+
? "Ready to track"
405+
: "Listening for hooks")
414406
.font(.caption2)
415407
.foregroundStyle(.secondary)
416408
}
417409
Spacer()
418-
if !tracking.liveSessions.isEmpty {
419-
Text("\(tracking.liveSessions.count)")
410+
if !tracking.hookFreshAgents.isEmpty {
411+
Text("\(tracking.hookFreshAgents.count)")
420412
.font(.caption2.weight(.semibold).monospacedDigit())
421413
.padding(.horizontal, 7)
422414
.padding(.vertical, 2)
@@ -542,25 +534,6 @@ struct PanelRootView: View {
542534
.background(tint.opacity(0.12), in: Capsule())
543535
}
544536

545-
/// Grace-period pill with a live SwiftUI countdown — updates every second
546-
/// without a separate Timer because Text(timerInterval:) drives itself.
547-
/// This is the ONE countdown the user ever sees in Auto mode.
548-
private func gracePill(endsAt: Date) -> some View {
549-
HStack(spacing: 6) {
550-
Image(systemName: "powersleep").font(.caption2)
551-
.accessibilityHidden(true)
552-
Text("Mac sleeps in ")
553-
.font(.caption2)
554-
Text(timerInterval: Date.now...endsAt, countsDown: true)
555-
.font(.caption2.monospacedDigit())
556-
}
557-
.foregroundStyle(Color.secondary)
558-
.padding(.horizontal, 9)
559-
.padding(.vertical, 5)
560-
.frame(maxWidth: .infinity, alignment: .leading)
561-
.background(Color.secondary.opacity(0.12), in: Capsule())
562-
}
563-
564537
@ViewBuilder
565538
private func compactAction(
566539
icon: String,

0 commit comments

Comments
 (0)