diff --git a/supacode/Clients/Dock/DockClient.swift b/supacode/Clients/Dock/DockClient.swift index 67bd10e8..41c354d1 100644 --- a/supacode/Clients/Dock/DockClient.swift +++ b/supacode/Clients/Dock/DockClient.swift @@ -5,18 +5,17 @@ import ComposableArchitecture /// and the attention bounce. Wrapping these in a dependency keeps the reducer /// testable and matches how the other AppKit side effects are injected. struct DockClient { - /// Show (`true`) or clear (`false`) the notification badge on the Dock tile. - var setNotificationBadge: @MainActor @Sendable (_ isVisible: Bool) -> Void + /// Set the Dock tile's notification badge to `count` unread items, or clear + /// it when `count` is zero. + var setNotificationBadge: @MainActor @Sendable (_ count: Int) -> Void /// Bounce the Dock icon according to the configured mode. `.off` is a no-op. var bounce: @MainActor @Sendable (_ mode: DockBounceMode) -> Void } extension DockClient: DependencyKey { static let liveValue = DockClient( - setNotificationBadge: { isVisible in - // An empty (but non-nil) label renders the bare red badge dot; `nil` - // hides it. A glyph inside the pill would just look redundant. - NSApplication.shared.dockTile.badgeLabel = isVisible ? "" : nil + setNotificationBadge: { count in + NSApplication.shared.dockTile.badgeLabel = count > 0 ? String(count) : nil }, bounce: { mode in switch mode { diff --git a/supacode/Features/App/Reducer/AppFeature.swift b/supacode/Features/App/Reducer/AppFeature.swift index 5b58451f..1d219ba3 100644 --- a/supacode/Features/App/Reducer/AppFeature.swift +++ b/supacode/Features/App/Reducer/AppFeature.swift @@ -237,7 +237,7 @@ struct AppFeature { await terminalClient.send(.setAgentDetectionEnabled(agentDetectionEnabled)) }, .run { _ in - await dockClient.setNotificationBadge(false) + await dockClient.setNotificationBadge(0) }, .run { send in for await event in await terminalClient.events() { @@ -498,7 +498,7 @@ struct AppFeature { settings: state.settings, customCommands: state.selectedCustomCommands ) - let showDot = settings.showNotificationDotOnDock && state.notificationIndicatorCount > 0 + let badgeCount = settings.showNotificationDotOnDock ? state.notificationIndicatorCount : 0 return .merge( .send(.repositories(.githubIntegration(.setGithubIntegrationEnabled(settings.githubIntegrationEnabled)))), .send( @@ -572,7 +572,7 @@ struct AppFeature { } }, .run { _ in - await dockClient.setNotificationBadge(showDot) + await dockClient.setNotificationBadge(badgeCount) } ) @@ -1172,7 +1172,7 @@ struct AppFeature { return .send(.updates(.debugSimulateUpdateFound)) case .commandPalette(.delegate(.debugLightDockNotificationDot)): - return .run { _ in await dockClient.setNotificationBadge(true) } + return .run { _ in await dockClient.setNotificationBadge(1) } #endif case .commandPalette: @@ -1212,9 +1212,9 @@ struct AppFeature { case .terminalEvent(.notificationIndicatorChanged(let count)): state.notificationIndicatorCount = count - let showDot = state.settings.showNotificationDotOnDock && count > 0 + let badgeCount = state.settings.showNotificationDotOnDock ? count : 0 return .run { _ in - await dockClient.setNotificationBadge(showDot) + await dockClient.setNotificationBadge(badgeCount) } case .terminalEvent(.runScriptStatusChanged(let worktreeID, let isRunning)): diff --git a/supacodeTests/AppFeatureDockTests.swift b/supacodeTests/AppFeatureDockTests.swift index bcca7e3d..49e7c493 100644 --- a/supacodeTests/AppFeatureDockTests.swift +++ b/supacodeTests/AppFeatureDockTests.swift @@ -78,13 +78,13 @@ struct AppFeatureDockTests { @Test(.dependencies) func indicatorChangeShowsBadgeWhenEnabledAndCountPositive() async { var globalSettings = GlobalSettings.default globalSettings.showNotificationDotOnDock = true - let badges = LockIsolated<[Bool]>([]) + let badges = LockIsolated<[Int]>([]) let store = TestStore( initialState: AppFeature.State(settings: SettingsFeature.State(settings: globalSettings)) ) { AppFeature() } withDependencies: { - $0.dockClient.setNotificationBadge = { isVisible in badges.withValue { $0.append(isVisible) } } + $0.dockClient.setNotificationBadge = { count in badges.withValue { $0.append(count) } } } store.exhaustivity = .off @@ -93,19 +93,19 @@ struct AppFeatureDockTests { } await store.finish() - #expect(badges.value == [true]) + #expect(badges.value == [2]) } @Test(.dependencies) func indicatorChangeClearsBadgeWhenDisabled() async { var globalSettings = GlobalSettings.default globalSettings.showNotificationDotOnDock = false - let badges = LockIsolated<[Bool]>([]) + let badges = LockIsolated<[Int]>([]) let store = TestStore( initialState: AppFeature.State(settings: SettingsFeature.State(settings: globalSettings)) ) { AppFeature() } withDependencies: { - $0.dockClient.setNotificationBadge = { isVisible in badges.withValue { $0.append(isVisible) } } + $0.dockClient.setNotificationBadge = { count in badges.withValue { $0.append(count) } } } store.exhaustivity = .off @@ -114,25 +114,25 @@ struct AppFeatureDockTests { } await store.finish() - #expect(badges.value == [false]) + #expect(badges.value == [0]) } @Test(.dependencies) func indicatorChangeClearsBadgeWhenCountZero() async { var globalSettings = GlobalSettings.default globalSettings.showNotificationDotOnDock = true - let badges = LockIsolated<[Bool]>([]) + let badges = LockIsolated<[Int]>([]) let store = TestStore( initialState: AppFeature.State(settings: SettingsFeature.State(settings: globalSettings)) ) { AppFeature() } withDependencies: { - $0.dockClient.setNotificationBadge = { isVisible in badges.withValue { $0.append(isVisible) } } + $0.dockClient.setNotificationBadge = { count in badges.withValue { $0.append(count) } } } store.exhaustivity = .off await store.send(.terminalEvent(.notificationIndicatorChanged(count: 0))) await store.finish() - #expect(badges.value == [false]) + #expect(badges.value == [0]) } } diff --git a/supacodeTests/CommandPaletteFeatureTests.swift b/supacodeTests/CommandPaletteFeatureTests.swift index 86978a49..e7ec5302 100644 --- a/supacodeTests/CommandPaletteFeatureTests.swift +++ b/supacodeTests/CommandPaletteFeatureTests.swift @@ -29,6 +29,7 @@ struct CommandPaletteFeatureTests { "debug.toast.inProgress", "debug.toast.success", "debug.update.simulate-found", + "debug.dock.notification-dot", ]) #endif expectNoDifference(items.map(\.id), expectedIDs) @@ -1655,7 +1656,7 @@ private func testCategory(for kind: CommandPaletteItem.Kind) -> CommandPaletteIt case .toggleLeftSidebar, .toggleActiveAgentsPanel, .toggleCanvas, .toggleShelf, .showDiff: return .view #if DEBUG - case .debugTestToast, .debugSimulateUpdateFound: + case .debugTestToast, .debugSimulateUpdateFound, .debugLightDockNotificationDot: return .debug #endif } @@ -1677,7 +1678,7 @@ private func testDefaultSuggestion(for kind: CommandPaletteItem.Kind) -> Bool { .deleteWorktree, .runCustomCommand: return false #if DEBUG - case .debugTestToast, .debugSimulateUpdateFound: + case .debugTestToast, .debugSimulateUpdateFound, .debugLightDockNotificationDot: return true #endif }