Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions DoomCoder.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
DC0000000000002000000202 /* AgentNotificationCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0000000000001000000202 /* AgentNotificationCatalog.swift */; };
DC0000000000002000000203 /* NotifyAboutCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0000000000001000000203 /* NotifyAboutCard.swift */; };
DC0000000000002000000701 /* ApprovalArbiter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0000000000001000000701 /* ApprovalArbiter.swift */; };
DC0000000000002000000800 /* SleepStateNotifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0000000000001000000800 /* SleepStateNotifier.swift */; };
DC0000000000002000000F01 /* GlassCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0000000000001000000F01 /* GlassCard.swift */; };
DC0000000000002000000F02 /* GlassButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0000000000001000000F02 /* GlassButtonStyle.swift */; };
DC0000000000002000000F03 /* AppRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC0000000000001000000F03 /* AppRouter.swift */; };
Expand Down Expand Up @@ -138,7 +137,6 @@
DC0000000000001000000202 /* AgentNotificationCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentNotificationCatalog.swift; sourceTree = "<group>"; };
DC0000000000001000000203 /* NotifyAboutCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotifyAboutCard.swift; sourceTree = "<group>"; };
DC0000000000001000000701 /* ApprovalArbiter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApprovalArbiter.swift; sourceTree = "<group>"; };
DC0000000000001000000800 /* SleepStateNotifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SleepStateNotifier.swift; sourceTree = "<group>"; };
DC0000000000001000000F01 /* GlassCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlassCard.swift; sourceTree = "<group>"; };
DC0000000000001000000F02 /* GlassButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlassButtonStyle.swift; sourceTree = "<group>"; };
DC0000000000001000000F03 /* AppRouter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouter.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -233,7 +231,6 @@
DC0000000000001000000025 /* HookSocketListener.swift */,
DC0000000000001000000032 /* AgentInstallerV2.swift */,
DC0000000000001000000027 /* NotificationDispatcher.swift */,
DC0000000000001000000800 /* SleepStateNotifier.swift */,
DC0000000000001000000028 /* AgentTrackingManager.swift */,
DC0000000000001000000039 /* ConfigureAgentsWindowV2.swift */,
DC0000000000001000000030 /* TrackAgentsPopover.swift */,
Expand Down Expand Up @@ -414,7 +411,6 @@
DC0000000000002000000025 /* HookSocketListener.swift in Sources */,
DC0000000000002000000032 /* AgentInstallerV2.swift in Sources */,
DC0000000000002000000027 /* NotificationDispatcher.swift in Sources */,
DC0000000000002000000800 /* SleepStateNotifier.swift in Sources */,
DC0000000000002000000028 /* AgentTrackingManager.swift in Sources */,
DC0000000000002000000039 /* ConfigureAgentsWindowV2.swift in Sources */,
DC0000000000002000000030 /* TrackAgentsPopover.swift in Sources */,
Expand Down Expand Up @@ -586,7 +582,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2702;
CURRENT_PROJECT_VERSION = 2703;
DEVELOPMENT_TEAM = A9P2388PHM;
ENABLE_HARDENED_RUNTIME = YES;
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
Expand All @@ -596,7 +592,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 2.7.2;
MARKETING_VERSION = 2.7.3;
PRODUCT_BUNDLE_IDENTIFIER = com.doomcoder.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand All @@ -614,7 +610,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2702;
CURRENT_PROJECT_VERSION = 2703;
DEVELOPMENT_TEAM = A9P2388PHM;
ENABLE_HARDENED_RUNTIME = YES;
GCC_TREAT_WARNINGS_AS_ERRORS = YES;
Expand All @@ -624,7 +620,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MARKETING_VERSION = 2.7.2;
MARKETING_VERSION = 2.7.3;
PRODUCT_BUNDLE_IDENTIFIER = com.doomcoder.app;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down
23 changes: 21 additions & 2 deletions DoomCoder/AgentTracking/AgentNotificationCatalog.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,15 @@ enum AgentNotificationCatalog {
case .cursor:
return [.completed, .failed, .waitingApproval, .waitingInput, .sessionStart, .toolCalls, .subagentActivity,
.fileEdits, .contextCompaction, .agentThinking, .housekeeping, .userPromptSent]
// `.housekeeping` is listed for every agent with a session-close event
// (see `isSessionClose`) so the toggle that re-enables close alerts is
// reachable in the editor.
case .vscode:
return [.completed, .failed, .waitingApproval, .sessionStart, .toolCalls, .subagentActivity,
.contextCompaction, .userPromptSent]
.contextCompaction, .housekeeping, .userPromptSent]
case .copilotCLI:
return [.completed, .failed, .waitingApproval, .sessionStart, .toolCalls, .subagentActivity,
.contextCompaction, .userPromptSent]
.contextCompaction, .housekeeping, .userPromptSent]
case .windsurf:
// Windsurf emits no SessionStart, no error phase, no subagent events.
return [.completed, .waitingApproval, .waitingInput, .toolCalls,
Expand Down Expand Up @@ -276,6 +279,22 @@ enum AgentNotificationCatalog {
}
}

/// True when a `.sessionEnd` event means "the user closed the session"
/// rather than "the agent finished its turn". Both normalize to
/// `.sessionEnd` on purpose so the session state machine still finalizes
/// (`hasEnded`, auto-revert, session history), but only the turn-end
/// variant is a "Task completed" alert. Closing an agent almost always
/// follows a real turn-end event, so notifying on both double-fires.
/// Routed to the default-off `housekeeping` switch by
/// `AgentNotificationPrefs.shouldNotify`.
static func isSessionClose(for agent: TrackedAgent, rawEvent: String) -> Bool {
switch agent {
case .claude, .vscode: return rawEvent == "SessionEnd"
case .cursor, .copilotCLI: return rawEvent == "sessionEnd"
case .windsurf, .codexCLI, .opencode: return false
}
}

/// Classifies a `.permissionNeeded` event into an approval-kind id for the
/// agent. Mirrors the per-agent logic in AgentEventNormalizer.swift.
static func approvalKindID(for agent: TrackedAgent, rawEvent: String, toolName: String?) -> String {
Expand Down
7 changes: 7 additions & 0 deletions DoomCoder/AgentTracking/AgentNotificationPrefs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,13 @@ struct AgentNotificationPrefs: Codable, Equatable, Sendable {
func shouldNotify(_ ev: NormalizedHookEvent) -> Bool {
switch ev.phase {
case .sessionEnd:
// Closing an agent is not "task completed" — it fires right after a
// real turn-end event and double-notifies. Gate it on the
// default-off housekeeping switch instead. The phase stays
// `.sessionEnd` so session finalization is unaffected.
if AgentNotificationCatalog.isSessionClose(for: ev.agent, rawEvent: ev.rawEvent) {
return housekeeping
}
return completed
case .error, .toolError:
return failed
Expand Down
108 changes: 0 additions & 108 deletions DoomCoder/AgentTracking/SleepStateNotifier.swift

This file was deleted.

18 changes: 1 addition & 17 deletions DoomCoder/SleepManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -390,19 +390,6 @@ final class SleepManager {
resetSessionTimer()
if mode == .screenOff { startScreenOff() }
notifyStateChanged()
// Notify user when Auto mode takes sleep control. The copy reflects
// WHY the Mac is held (agents / user activity / snooze) so the user
// can tell at a glance.
if keepAwakeMode == .auto {
let reason: SleepStateNotifier.TakeReason
if isSnoozed {
reason = .snoozed
} else {
let names = autoStatusLines.map(\.agentDisplayName)
reason = names.isEmpty ? .userActive : .agents(names)
}
SleepStateNotifier.shared.notifyTookControl(reason: reason)
}
}

private func releaseAssertion() {
Expand Down Expand Up @@ -600,10 +587,7 @@ final class SleepManager {
if shouldHoldAuto {
if !isActive { acquireAssertion() }
} else {
if isActive {
releaseAssertion()
SleepStateNotifier.shared.notifyReleasedControl()
}
if isActive { releaseAssertion() }
}
}

Expand Down
32 changes: 32 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

---

## [2.7.3] - 2026-08-12

macOS only. The iOS companion is unchanged and stays on its current
App Store version.

### Fixed — notification spam

- **No more "Keeping Mac Awake" / "Sleep Control Returned" banners.**
Both fired on every `isActive` transition in Auto mode, and the hold
condition is an OR of three volatile signals with no hysteresis and no
debounce. Every walk-away-and-come-back cycle cost two notifications,
and toggling Auto off and back on re-fired the first one. They were
also the only notifications in the app that bypassed the preference
gate entirely, so there was no way to switch them off. Sleep state is
already live in the menu bar icon, the panel, and the iOS Dashboard via
`MacStatus.sleepActive`, so the banners were repeating what the UI
already showed. `SleepStateNotifier` is removed.
- **Closing an agent no longer fires "Task completed".** A session-close
event (`SessionEnd` / `sessionEnd`) normalizes to the same
`.sessionEnd` phase as a turn-end event (`Stop` / `agentStop`), so
quitting Claude Code, Cursor, VS Code Copilot, or Copilot CLI produced
a duplicate "done" alert seconds after the real one. Close events are
now gated on the default-off **Housekeeping** switch instead. The phase
is unchanged, so session finalization, auto-revert, and session history
all behave exactly as before, and the event is still recorded and
visible in Activity. Turn-end alerts are untouched. Flip Housekeeping
on in Configure > Agents to get close alerts back.
- **Housekeeping toggle now shown for VS Code Copilot and Copilot CLI**,
so the switch that re-enables close alerts is reachable for those two.

---

## [2.7.2] - 2026-06-09

### Added
Expand Down
Loading