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)