Skip to content
Closed
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
23 changes: 17 additions & 6 deletions src/main/windows/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
BrowserWindow,
Notification,
shell,
nativeTheme,
ipcMain,
Expand Down Expand Up @@ -100,26 +101,36 @@ 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) {
if (win.isMinimized()) win.restore()
win.focus()
}
})

notification.show()
} catch (error) {
console.error("[Main] Failed to show notification:", error)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
defaultAgentModeAtom,
desktopNotificationsEnabledAtom,
extendedThinkingEnabledAtom,
notifyWhenFocusedAtom,
soundNotificationsEnabledAtom,
preferredEditorAtom,
type AgentMode,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -273,6 +275,21 @@ export function AgentsPreferencesTab() {
</div>
<Switch checked={soundEnabled} onCheckedChange={setSoundEnabled} />
</div>
<div className="flex items-center justify-between p-4 border-t border-border">
<div className="flex flex-col space-y-1">
<span className="text-sm font-medium text-foreground">
Notify When Focused
</span>
<span className="text-xs text-muted-foreground">
Show notifications even when the app window is in the foreground
</span>
</div>
<Switch
checked={notifyWhenFocused}
onCheckedChange={setNotifyWhenFocused}
disabled={!desktopNotificationsEnabled}
/>
</div>
</div>

{/* Navigation */}
Expand Down
19 changes: 9 additions & 10 deletions src/renderer/features/agents/hooks/use-desktop-notifications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<number>(0)
Expand Down Expand Up @@ -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
Expand All @@ -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<NotificationPermission> => {
if (isDesktopApp()) {
Expand Down
18 changes: 12 additions & 6 deletions src/renderer/features/agents/main/active-chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down
10 changes: 10 additions & 0 deletions src/renderer/lib/atoms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,16 @@ export const desktopNotificationsEnabledAtom = atomWithStorage<boolean>(
{ 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<boolean>(
"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)
Expand Down