diff --git a/src/main/windows/main.ts b/src/main/windows/main.ts index f1997d39e..44763be07 100644 --- a/src/main/windows/main.ts +++ b/src/main/windows/main.ts @@ -1,5 +1,6 @@ import { BrowserWindow, + Notification, shell, nativeTheme, ipcMain, @@ -100,19 +101,27 @@ function registerIpcHandlers(): void { "app:show-notification", (event, options: { title: string; body: string }) => { try { - const { Notification } = require("electron") - const iconPath = join(__dirname, "../../../build/icon.ico") - const icon = existsSync(iconPath) ? nativeImage.createFromPath(iconPath) : undefined + if (!Notification.isSupported()) { + console.warn("[Main] Notifications not supported on this system") + return + } + + // On macOS, the app icon is used automatically — no custom icon needed. + // On Windows, use .ico; on Linux, use .png. + let icon: Electron.NativeImage | undefined + if (process.platform !== "darwin") { + const ext = process.platform === "win32" ? "icon.ico" : "icon.png" + const iconPath = join(__dirname, "../../build", ext) + icon = existsSync(iconPath) ? nativeImage.createFromPath(iconPath) : undefined + } const notification = new Notification({ title: options.title, body: options.body, - icon, + ...(icon && { icon }), ...(process.platform === "win32" && { silent: false }), }) - notification.show() - notification.on("click", () => { const win = getWindowFromEvent(event) if (win) { @@ -120,6 +129,8 @@ function registerIpcHandlers(): void { win.focus() } }) + + notification.show() } catch (error) { console.error("[Main] Failed to show notification:", error) } diff --git a/src/renderer/components/dialogs/settings-tabs/agents-preferences-tab.tsx b/src/renderer/components/dialogs/settings-tabs/agents-preferences-tab.tsx index 87cd234e7..13573e88e 100644 --- a/src/renderer/components/dialogs/settings-tabs/agents-preferences-tab.tsx +++ b/src/renderer/components/dialogs/settings-tabs/agents-preferences-tab.tsx @@ -7,6 +7,7 @@ import { defaultAgentModeAtom, desktopNotificationsEnabledAtom, extendedThinkingEnabledAtom, + notifyWhenFocusedAtom, soundNotificationsEnabledAtom, preferredEditorAtom, type AgentMode, @@ -146,6 +147,7 @@ export function AgentsPreferencesTab() { ) const [soundEnabled, setSoundEnabled] = useAtom(soundNotificationsEnabledAtom) const [desktopNotificationsEnabled, setDesktopNotificationsEnabled] = useAtom(desktopNotificationsEnabledAtom) + const [notifyWhenFocused, setNotifyWhenFocused] = useAtom(notifyWhenFocusedAtom) const [analyticsOptOut, setAnalyticsOptOut] = useAtom(analyticsOptOutAtom) const [ctrlTabTarget, setCtrlTabTarget] = useAtom(ctrlTabTargetAtom) const [autoAdvanceTarget, setAutoAdvanceTarget] = useAtom(autoAdvanceTargetAtom) @@ -273,6 +275,21 @@ export function AgentsPreferencesTab() { +
+
+ + Notify When Focused + + + Show notifications even when the app window is in the foreground + +
+ +
{/* Navigation */} diff --git a/src/renderer/features/agents/hooks/use-desktop-notifications.ts b/src/renderer/features/agents/hooks/use-desktop-notifications.ts index f64338c3f..19bf42f31 100644 --- a/src/renderer/features/agents/hooks/use-desktop-notifications.ts +++ b/src/renderer/features/agents/hooks/use-desktop-notifications.ts @@ -6,7 +6,7 @@ import { useCallback, useRef, useEffect } from "react" import { useAtomValue } from "jotai" import { isDesktopApp } from "../../../lib/utils/platform" -import { desktopNotificationsEnabledAtom } from "../../../lib/atoms" +import { desktopNotificationsEnabledAtom, notifyWhenFocusedAtom } from "../../../lib/atoms" // throttle interval to prevent notification spam (ms) const NOTIFICATION_THROTTLE_MS = 3000 @@ -30,6 +30,7 @@ export interface NotificationOptions { export function useDesktopNotifications() { const notificationsEnabled = useAtomValue(desktopNotificationsEnabledAtom) + const notifyWhenFocused = useAtomValue(notifyWhenFocusedAtom) // track last notification time to throttle rapid-fire notifications const lastNotificationTime = useRef(0) @@ -98,15 +99,15 @@ export function useDesktopNotifications() { }, [notificationsEnabled]) const notifyAgentComplete = useCallback((chatName: string) => { - // don't notify if window is focused - user is already watching - if (document.hasFocus()) { + // Skip if window is focused and user hasn't opted into focused notifications + if (!notifyWhenFocused && document.hasFocus()) { return } const title = "Agent Complete" const body = chatName ? `Finished working on "${chatName}"` : "Agent has completed its task" showNotification(title, body, { priority: "complete" }) - }, [showNotification]) + }, [showNotification, notifyWhenFocused]) const notifyAgentError = useCallback((errorMessage: string) => { // always notify on errors, even if window is focused @@ -116,26 +117,24 @@ export function useDesktopNotifications() { }, [showNotification]) const notifyAgentNeedsInput = useCallback((chatName: string) => { - // don't notify if window is focused - if (document.hasFocus()) { + if (!notifyWhenFocused && document.hasFocus()) { return } const title = "Input Required" const body = chatName ? `"${chatName}" is waiting for your input` : "Agent is waiting for your input" showNotification(title, body, { priority: "input" }) - }, [showNotification]) + }, [showNotification, notifyWhenFocused]) const notifyPlanReady = useCallback((chatName: string) => { - // don't notify if window is focused - if (document.hasFocus()) { + if (!notifyWhenFocused && document.hasFocus()) { return } const title = "Plan Ready" const body = chatName ? `"${chatName}" has a plan ready for approval` : "A plan is ready for your approval" showNotification(title, body, { priority: "plan" }) - }, [showNotification]) + }, [showNotification, notifyWhenFocused]) const requestPermission = useCallback(async (): Promise => { if (isDesktopApp()) { diff --git a/src/renderer/features/agents/main/active-chat.tsx b/src/renderer/features/agents/main/active-chat.tsx index 24ba548ee..69f1dee95 100644 --- a/src/renderer/features/agents/main/active-chat.tsx +++ b/src/renderer/features/agents/main/active-chat.tsx @@ -6102,12 +6102,15 @@ Make sure to preserve all functionality from both branches when resolving confli // Ignore audio errors } } - - // Show native notification (desktop app, when window not focused) - notifyAgentComplete(agentChat?.name || "Agent") } } + // Show native notification if not manually aborted + // (the hook handles focus/preference checks internally) + if (!wasManuallyAborted) { + notifyAgentComplete(agentChat?.name || "Agent") + } + // Refresh diff stats after agent finishes making changes fetchDiffStatsRef.current() @@ -6298,12 +6301,15 @@ Make sure to preserve all functionality from both branches when resolving confli // Ignore audio errors } } - - // Show native notification (desktop app, when window not focused) - notifyAgentComplete(agentChat?.name || "Agent") } } + // Show native notification if not manually aborted + // (the hook handles focus/preference checks internally) + if (!wasManuallyAborted) { + notifyAgentComplete(agentChat?.name || "Agent") + } + // Refresh diff stats after agent finishes making changes fetchDiffStatsRef.current() diff --git a/src/renderer/lib/atoms/index.ts b/src/renderer/lib/atoms/index.ts index 9ab30b6fb..18ddfe83a 100644 --- a/src/renderer/lib/atoms/index.ts +++ b/src/renderer/lib/atoms/index.ts @@ -396,6 +396,16 @@ export const desktopNotificationsEnabledAtom = atomWithStorage( { getOnInit: true }, ) +// Preferences - Notify When Focused +// When enabled, show desktop notifications even when the app window is focused +// (e.g. when working in a different chat). When disabled, only notify when the app is in the background. +export const notifyWhenFocusedAtom = atomWithStorage( + "preferences:notify-when-focused", + false, + undefined, + { getOnInit: true }, +) + // Preferences - Windows Window Frame Style // When true, uses native frame (standard Windows title bar) // When false, uses frameless window (dark custom title bar)