How PI-Desktop Handles Native Notifications Using Its Focus-Aware Policy
PI-Desktop delivers native OS notifications through Electron's Notification API while enforcing a strict focus-aware policy that suppresses task-type alerts when the window is active and manages interactive prompts based on session visibility.
PI-Desktop, an open-source Electron application framework maintained in the vastsa/PI-Desktop repository, implements a sophisticated native notification architecture that minimizes user interruption. The system distinguishes between transient OS-level banners and durable in-app inbox messages through a policy defined in Architecture Decision Records (ADRs). By leveraging Electron's IPC channels and permission-gated plugin capabilities, the framework ensures native alerts respect both user focus state and explicit consent.
IPC Protocol for Native Notifications
The communication between renderer processes and the main Electron host begins with a strictly typed IPC protocol. In packages/shared/src/protocol.ts, the shared protocol file defines the notification/showNative channel that carries notification payloads from the UI or plugins to the host.
The method signature requires a payload containing title, an optional body, and a kind field that accepts either "task" or "interactive" as values. This discriminator field, defined around lines 68-69 of the protocol file, determines the subsequent focus-aware behavior enforced by the host process.
Focus-Aware Policy Enforcement
PI-Desktop's notification policy, detailed in docs/adr/0187-focus-aware-native-task-notifications.md, implements distinct visibility rules based on the notification type and current application focus state. The host-side logic in apps/desktop/electron/main/ipc/notification-ipc.ts evaluates these rules before invoking new Notification({...}).
Task-Type Notifications
Task-type notifications (kind: "task") represent completed background operations like file backups or data syncs. According to the ADR, these are shown only when the main window is not focused, including when a background session is active. This prevents redundant banner noise while the user is already interacting with the application and viewing task completion in the UI.
Interactive-Type Notifications
Interactive-type notifications (kind: "interactive") require immediate user attention, such as permission requests or approval prompts. These are suppressed only when the exact session that generated the request is visible and focused. If another session has focus, the banner is still displayed, ensuring critical prompts reach the user regardless of which part of the application they are currently viewing.
Host-Side Implementation
The Electron main process handles native notification delivery through a pipeline involving the plugin runtime and dedicated IPC services. In apps/desktop/electron/main/plugin-runtime.ts, incoming requests from plugins are validated and forwarded to the notification service, which checks permissions and focus states before displaying OS-level toasts.
The implementation returns a { shown: boolean } result to the renderer, allowing client code to react to suppression events. The renderer-side wrapper in apps/desktop/src/lib/api.ts uses a generic invoke helper to forward requests and surface host-side errors as exceptions.
// Host-side (Electron main) – handling the IPC call
async function showPluginNativeNotification(input: {
title: string; body?: string; kind?: "task" | "interactive";
}) {
// Permission verified by plugin-runtime layer
const win = electron.BrowserWindow.getFocusedWindow();
const shouldShow = decideVisibility(input.kind, win?.isFocused() ?? false);
if (!shouldShow) return { shown: false };
new electron.Notification({
title: input.title,
body: input.body ?? "",
}).show();
return { shown: true };
}
Plugin Permission Model
Before plugins can trigger native notifications, they must declare the capability in their manifest and obtain explicit user permission, as specified in docs/adr/0074-native-notification-permission-for-plugins.md. The permission gate ensures that third-party extensions cannot spam users with OS-level alerts without consent.
Plugin-originated notifications flow through the same native channel as core application notifications but are never persisted to the in-app inbox. This separation maintains the distinction between transient alerts and durable notification history.
// Plugin manifest entry – declare notification capability
{
"capabilities": ["notify"], // grants permission to call ui.showNativeNotification
"ui": {
"showNativeNotification": true
}
}
Client-Side Usage Examples
Applications and plugins interact with the notification system through the pi.ui.showNativeNotification API. The renderer process constructs a payload matching the protocol and awaits the boolean result indicating whether the notification was actually displayed.
// Renderer-side: request a native notification for a completed task
await pi.ui.showNativeNotification({
title: "Backup Finished",
body: "Your files have been saved.",
kind: "task", // ← task-type => suppressed if app is focused
});
// Renderer-side: request a native notification for an interactive ask
await pi.ui.showNativeNotification({
title: "Approve Access",
body: "A plugin needs permission to read your contacts.",
kind: "interactive", // ← interactive-type => shown unless this session is focused
});
Summary
- PI-Desktop uses Electron's
NotificationAPI routed through thenotification/showNativeIPC channel defined inpackages/shared/src/protocol.ts. - Focus-aware policy distinguishes notification types: Task notifications appear only when the app is unfocused, while interactive notifications appear unless their originating session is focused.
- Plugin notifications require explicit permission via the
notifycapability declared in plugin manifests, as documented in ADR 0074. - Host-side enforcement occurs in
apps/desktop/electron/main/ipc/notification-ipc.ts, which returns{ shown: boolean }to indicate display status. - In-app inbox separation ensures native banners never duplicate entries in the persistent notification store managed by
apps/desktop/src/stores/runtime/notification-runtime.ts.
Frequently Asked Questions
What is the difference between task and interactive notification types in PI-Desktop?
Task-type notifications (kind: "task") represent background operation completions and are suppressed when the main window is focused to avoid interrupting the user. Interactive-type notifications (kind: "interactive") represent prompts requiring immediate attention and are only suppressed when the specific session that generated them is already visible and focused, ensuring critical alerts reach the user even when working in other parts of the application.
How do plugins request permission to send native notifications?
Plugins must declare the "notify" capability in their manifest file and set "ui.showNativeNotification": true. At runtime, the plugin must call ui.requestNotificationPermission() before invoking ui.showNativeNotification(). The permission state is validated by the plugin runtime bridge in apps/desktop/electron/main/plugin-runtime.ts before forwarding to the host notification service.
Why does PI-Desktop suppress task notifications when the application is focused?
According to ADR 0187, displaying native banners for completed background tasks while the user is actively viewing the application creates redundant noise. Since the task completion is immediately visible in the UI and recorded in the in-app notification inbox, the native toast is withheld to respect the user's current attention context.
What happens when a notification is suppressed due to focus rules?
The IPC call returns { shown: false } to the renderer, allowing the calling code to detect the suppression event. The notification is not queued or deferred; it is simply dropped, as the user is assumed to have seen the equivalent information through the focused application interface or will encounter it in the notification inbox later.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →