mirror of
https://github.com/davidkaya/aryx.git
synced 2026-08-06 19:58:43 +02:00
feat: add desktop notifications for run completion
Show native OS notifications when a session run completes, fails, or needs approval while the app window is unfocused. Clicking a notification focuses the window and selects the relevant session. Includes a toggle in Settings > Appearance to enable/disable notifications (enabled by default). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -257,6 +257,11 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
return this.workspace;
|
||||
}
|
||||
|
||||
/** Returns the in-memory workspace without loading from disk. Used for synchronous checks. */
|
||||
getCachedWorkspace(): WorkspaceState | undefined {
|
||||
return this.workspace;
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
this.ptyManager.dispose();
|
||||
await this.sidecar.dispose();
|
||||
@@ -503,6 +508,12 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async setNotificationsEnabled(enabled: boolean): Promise<WorkspaceState> {
|
||||
const workspace = await this.loadWorkspace();
|
||||
workspace.settings.notificationsEnabled = enabled;
|
||||
return this.persistAndBroadcast(workspace);
|
||||
}
|
||||
|
||||
async describeTerminal(): Promise<TerminalSnapshot | undefined> {
|
||||
return this.ptyManager.getSnapshot();
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import type { QuerySessionsInput } from '@shared/domain/sessionLibrary';
|
||||
import type { AppearanceTheme } from '@shared/domain/tooling';
|
||||
|
||||
import { AryxAppService } from '@main/AryxAppService';
|
||||
import { createDesktopNotificationHandler } from '@main/services/desktopNotifications';
|
||||
import { applyTitleBarTheme } from '@main/windows/titleBarTheme';
|
||||
|
||||
const { ipcMain } = electron;
|
||||
@@ -86,6 +87,10 @@ export function registerIpcHandlers(window: BrowserWindow, service: AryxAppServi
|
||||
ipcChannels.setTerminalHeight,
|
||||
(_event, input: SetTerminalHeightInput) => service.setTerminalHeight(input.height),
|
||||
);
|
||||
ipcMain.handle(
|
||||
ipcChannels.setNotificationsEnabled,
|
||||
(_event, enabled: boolean) => service.setNotificationsEnabled(enabled),
|
||||
);
|
||||
ipcMain.handle(ipcChannels.saveMcpServer, (_event, input: SaveMcpServerInput) =>
|
||||
service.saveMcpServer(input.server),
|
||||
);
|
||||
@@ -183,6 +188,14 @@ export function registerIpcHandlers(window: BrowserWindow, service: AryxAppServi
|
||||
window.webContents.send(ipcChannels.sessionEvent, event);
|
||||
});
|
||||
|
||||
// Desktop notifications for run completion, failure, and approval requests
|
||||
const handleNotification = createDesktopNotificationHandler(
|
||||
() => window,
|
||||
() => service.getCachedWorkspace(),
|
||||
(sessionId) => service.selectSession(sessionId),
|
||||
);
|
||||
service.on('session-event', handleNotification);
|
||||
|
||||
service.on('terminal-data', (data) => {
|
||||
window.webContents.send(ipcChannels.terminalData, data);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import electron from 'electron';
|
||||
import type { BrowserWindow } from 'electron';
|
||||
|
||||
import type { SessionEventRecord } from '@shared/domain/event';
|
||||
import type { WorkspaceState } from '@shared/domain/workspace';
|
||||
|
||||
const { Notification } = electron;
|
||||
|
||||
/**
|
||||
* Creates a handler that shows native OS notifications for session run
|
||||
* completions, failures, and approval requests when the window is unfocused.
|
||||
*
|
||||
* Clicking a notification focuses the window and selects the relevant session.
|
||||
*/
|
||||
export function createDesktopNotificationHandler(
|
||||
getWindow: () => BrowserWindow | undefined,
|
||||
getWorkspace: () => WorkspaceState | undefined,
|
||||
selectSession: (sessionId: string) => Promise<WorkspaceState>,
|
||||
): (event: SessionEventRecord) => void {
|
||||
const runningSessions = new Set<string>();
|
||||
const notifiedApprovals = new Set<string>();
|
||||
|
||||
return (event: SessionEventRecord) => {
|
||||
const window = getWindow();
|
||||
if (window?.isFocused()) return;
|
||||
|
||||
const workspace = getWorkspace();
|
||||
if (workspace?.settings.notificationsEnabled === false) return;
|
||||
|
||||
if (!Notification.isSupported()) return;
|
||||
|
||||
const session = workspace?.sessions.find((s) => s.id === event.sessionId);
|
||||
const sessionTitle = session?.title ?? 'Session';
|
||||
|
||||
// Track running sessions to detect completion/failure transitions
|
||||
if (event.kind === 'status') {
|
||||
if (event.status === 'running') {
|
||||
runningSessions.add(event.sessionId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!runningSessions.has(event.sessionId)) return;
|
||||
runningSessions.delete(event.sessionId);
|
||||
|
||||
if (event.status === 'idle') {
|
||||
showNotification('Run completed', sessionTitle, event.sessionId, window, selectSession);
|
||||
} else if (event.status === 'error') {
|
||||
showNotification('Run failed', sessionTitle, event.sessionId, window, selectSession);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Detect new approval requests from run-updated events
|
||||
if (event.kind === 'run-updated' && event.run) {
|
||||
const approvalEvent = [...event.run.events]
|
||||
.reverse()
|
||||
.find((e) => e.kind === 'approval' && e.status === 'running');
|
||||
|
||||
if (approvalEvent?.approvalId && !notifiedApprovals.has(approvalEvent.approvalId)) {
|
||||
notifiedApprovals.add(approvalEvent.approvalId);
|
||||
const body = approvalEvent.approvalTitle
|
||||
? `${sessionTitle}: ${approvalEvent.approvalTitle}`
|
||||
: sessionTitle;
|
||||
showNotification('Approval needed', body, event.sessionId, window, selectSession);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function showNotification(
|
||||
title: string,
|
||||
body: string,
|
||||
sessionId: string,
|
||||
window: BrowserWindow | undefined,
|
||||
selectSession: (sessionId: string) => Promise<WorkspaceState>,
|
||||
): void {
|
||||
const notification = new Notification({ title, body, silent: false });
|
||||
|
||||
notification.on('click', () => {
|
||||
window?.show();
|
||||
window?.focus();
|
||||
void selectSession(sessionId);
|
||||
});
|
||||
|
||||
notification.show();
|
||||
}
|
||||
Reference in New Issue
Block a user