Skip to content

Commit a737bc3

Browse files
Croissant Le DouxCroissant Le Doux
authored andcommitted
Addressed PR comments for notifications
1 parent 4c0a250 commit a737bc3

11 files changed

Lines changed: 88 additions & 41 deletions

File tree

electron/ipc/register.ts

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -323,24 +323,27 @@ export function registerAllHandlers(win: BrowserWindow): void {
323323
cancelAskAboutCode(args.requestId);
324324
});
325325

326-
// --- Notifications ---
327-
ipcMain.handle(IPC.ShowNotification, (_e, args) => {
328-
assertString(args.title, 'title');
329-
assertString(args.body, 'body');
330-
assertString(args.threadId, 'threadId');
331-
assertStringArray(args.taskIds, 'taskIds');
332-
const notification = new Notification({
333-
title: args.title,
334-
body: args.body,
335-
});
336-
notification.on('click', () => {
337-
if (!win.isDestroyed()) {
338-
win.show();
339-
win.focus();
340-
win.webContents.send(IPC.NotificationClicked, { taskIds: args.taskIds });
341-
}
342-
});
343-
notification.show();
326+
// --- Notifications (fire-and-forget via ipcMain.on) ---
327+
ipcMain.on(IPC.ShowNotification, (_e, args) => {
328+
try {
329+
assertString(args.title, 'title');
330+
assertString(args.body, 'body');
331+
assertStringArray(args.taskIds, 'taskIds');
332+
const notification = new Notification({
333+
title: args.title,
334+
body: args.body,
335+
});
336+
notification.on('click', () => {
337+
if (!win.isDestroyed()) {
338+
win.show();
339+
win.focus();
340+
win.webContents.send(IPC.NotificationClicked, { taskIds: args.taskIds });
341+
}
342+
});
343+
notification.show();
344+
} catch (err) {
345+
console.warn('ShowNotification failed:', err);
346+
}
344347
});
345348

346349
// --- Window management ---

electron/preload.cjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ contextBridge.exposeInMainWorld('electron', {
9494
if (!isAllowedChannel(channel)) throw new Error(`Blocked IPC channel: ${channel}`);
9595
return ipcRenderer.invoke(channel, ...args);
9696
},
97+
send: (channel, ...args) => {
98+
if (!isAllowedChannel(channel)) throw new Error(`Blocked IPC channel: ${channel}`);
99+
ipcRenderer.send(channel, ...args);
100+
},
97101
on: (channel, listener) => {
98102
if (!isAllowedChannel(channel)) throw new Error(`Blocked IPC channel: ${channel}`);
99103
const wrapped = (_event, ...eventArgs) => listener(...eventArgs);

package-lock.json

Lines changed: 13 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/SettingsDialog.tsx

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
setThemePreset,
1010
setAutoTrustFolders,
1111
setShowPlans,
12+
setDesktopNotificationsEnabled,
1213
setInactiveColumnOpacity,
1314
setEditorCommand,
1415
} from '../store/store';
@@ -177,6 +178,31 @@ export function SettingsDialog(props: SettingsDialogProps) {
177178
</span>
178179
</div>
179180
</label>
181+
<label
182+
style={{
183+
display: 'flex',
184+
'align-items': 'center',
185+
gap: '10px',
186+
cursor: 'pointer',
187+
padding: '8px 12px',
188+
'border-radius': '8px',
189+
background: theme.bgInput,
190+
border: `1px solid ${theme.border}`,
191+
}}
192+
>
193+
<input
194+
type="checkbox"
195+
checked={store.desktopNotificationsEnabled}
196+
onChange={(e) => setDesktopNotificationsEnabled(e.currentTarget.checked)}
197+
style={{ 'accent-color': theme.accent, cursor: 'pointer' }}
198+
/>
199+
<div style={{ display: 'flex', 'flex-direction': 'column', gap: '2px' }}>
200+
<span style={{ 'font-size': '13px', color: theme.fg }}>Desktop notifications</span>
201+
<span style={{ 'font-size': '11px', color: theme.fgSubtle }}>
202+
Show native notifications when tasks finish or need attention
203+
</span>
204+
</div>
205+
</label>
180206
</div>
181207

182208
<div style={{ display: 'flex', 'flex-direction': 'column', gap: '10px' }}>

src/lib/ipc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ declare global {
77
electron: {
88
ipcRenderer: {
99
invoke: (channel: string, ...args: unknown[]) => Promise<unknown>;
10+
send: (channel: string, ...args: unknown[]) => void;
1011
on: (channel: string, listener: (...args: unknown[]) => void) => () => void;
1112
removeAllListeners: (channel: string) => void;
1213
};

src/store/core.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export const [store, setStore] = createStore<AppStore>({
4141
windowState: null,
4242
autoTrustFolders: false,
4343
showPlans: true,
44+
desktopNotificationsEnabled: false,
4445
inactiveColumnOpacity: 0.6,
4546
editorCommand: '',
4647
newTaskDropUrl: null,

src/store/desktopNotifications.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { createEffect, onCleanup, type Accessor } from 'solid-js';
22
import { store } from './store';
33
import { getTaskDotStatus, type TaskDotStatus } from './taskStatus';
44
import { setActiveTask } from './navigation';
5-
import { invoke } from '../lib/ipc';
65
import { IPC } from '../../electron/ipc/channels';
76

87
const DEBOUNCE_MS = 3_000;
@@ -34,12 +33,11 @@ export function startDesktopNotificationWatcher(windowFocused: Accessor<boolean>
3433
ready.length === 1
3534
? `${taskName(taskIds[0])} is ready for review`
3635
: `${ready.length} tasks ready for review`;
37-
invoke(IPC.ShowNotification, {
36+
window.electron.ipcRenderer.send(IPC.ShowNotification, {
3837
title: 'Task Ready',
3938
body,
40-
threadId: 'pc-task-ready',
4139
taskIds,
42-
}).catch(console.warn);
40+
});
4341
}
4442

4543
if (waiting.length > 0) {
@@ -48,12 +46,11 @@ export function startDesktopNotificationWatcher(windowFocused: Accessor<boolean>
4846
waiting.length === 1
4947
? `${taskName(taskIds[0])} needs your attention`
5048
: `${waiting.length} tasks need your attention`;
51-
invoke(IPC.ShowNotification, {
49+
window.electron.ipcRenderer.send(IPC.ShowNotification, {
5250
title: 'Task Waiting',
5351
body,
54-
threadId: 'pc-task-waiting',
5552
taskIds,
56-
}).catch(console.warn);
53+
});
5754
}
5855
}
5956

@@ -62,6 +59,7 @@ export function startDesktopNotificationWatcher(windowFocused: Accessor<boolean>
6259
}
6360

6461
function scheduleBatch(notification: PendingNotification): void {
62+
if (!store.desktopNotificationsEnabled) return;
6563
pending.push(notification);
6664
if (debounceTimer === undefined) {
6765
debounceTimer = setTimeout(flushNotifications, DEBOUNCE_MS);
@@ -111,9 +109,10 @@ export function startDesktopNotificationWatcher(windowFocused: Accessor<boolean>
111109
const offNotificationClicked = window.electron.ipcRenderer.on(
112110
IPC.NotificationClicked,
113111
(data: unknown) => {
114-
const msg = data as { taskIds: string[] };
115-
if (msg.taskIds?.length) {
116-
setActiveTask(msg.taskIds[0]);
112+
const msg = data as Record<string, unknown>;
113+
const taskIds = Array.isArray(msg?.taskIds) ? (msg.taskIds as string[]) : [];
114+
if (taskIds.length) {
115+
setActiveTask(taskIds[0]);
117116
}
118117
},
119118
);

src/store/persistence.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export async function saveState(): Promise<void> {
4040
windowState: store.windowState ? { ...store.windowState } : undefined,
4141
autoTrustFolders: store.autoTrustFolders,
4242
showPlans: store.showPlans,
43+
desktopNotificationsEnabled: store.desktopNotificationsEnabled,
4344
inactiveColumnOpacity: store.inactiveColumnOpacity,
4445
editorCommand: store.editorCommand || undefined,
4546
customAgents: store.customAgents.length > 0 ? [...store.customAgents] : undefined,
@@ -171,6 +172,7 @@ interface LegacyPersistedState {
171172
windowState?: unknown;
172173
autoTrustFolders?: unknown;
173174
showPlans?: unknown;
175+
desktopNotificationsEnabled?: unknown;
174176
inactiveColumnOpacity?: unknown;
175177
editorCommand?: unknown;
176178
customAgents?: unknown;
@@ -269,6 +271,10 @@ export async function loadState(): Promise<void> {
269271
s.windowState = parsePersistedWindowState(raw.windowState);
270272
s.autoTrustFolders = typeof raw.autoTrustFolders === 'boolean' ? raw.autoTrustFolders : false;
271273
s.showPlans = typeof raw.showPlans === 'boolean' ? raw.showPlans : true;
274+
s.desktopNotificationsEnabled =
275+
typeof raw.desktopNotificationsEnabled === 'boolean'
276+
? raw.desktopNotificationsEnabled
277+
: false;
272278
const rawOpacity = raw.inactiveColumnOpacity;
273279
s.inactiveColumnOpacity =
274280
typeof rawOpacity === 'number' &&

0 commit comments

Comments
 (0)