Desktop notifications no longer duplicate when native delivery succeeds Reasoning chain-of-thought is excluded from notification body text Untyped message parts are ignored in notification text extraction
110 lines
3.0 KiB
JavaScript
110 lines
3.0 KiB
JavaScript
export const createNotificationEmitterRuntime = (dependencies) => {
|
|
const {
|
|
process,
|
|
getDesktopNotifyEnabled,
|
|
desktopNotifyPrefix,
|
|
getUiNotificationClients,
|
|
getBroadcastGlobalUiEvent,
|
|
// Optional: in-process desktop shells (Electron main) inject a callback so
|
|
// notifications are delivered as a direct function call instead of a stdout
|
|
// stringly-typed IPC.
|
|
onDesktopNotification: initialOnDesktopNotification,
|
|
} = dependencies;
|
|
|
|
// Late-bindable: main() in server/index.js may call setOnDesktopNotification
|
|
// after runtime construction so the in-process shell can subscribe without
|
|
// restructuring the module-level wiring.
|
|
let onDesktopNotification = typeof initialOnDesktopNotification === 'function'
|
|
? initialOnDesktopNotification
|
|
: null;
|
|
|
|
const setOnDesktopNotification = (cb) => {
|
|
onDesktopNotification = typeof cb === 'function' ? cb : null;
|
|
};
|
|
|
|
const writeSseEvent = (res, payload) => {
|
|
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
|
};
|
|
|
|
const emitDesktopNotification = (payload) => {
|
|
const desktopNotifyEnabled = getDesktopNotifyEnabled();
|
|
if (!desktopNotifyEnabled) {
|
|
return false;
|
|
}
|
|
|
|
if (!payload || typeof payload !== 'object') {
|
|
return false;
|
|
}
|
|
|
|
if (onDesktopNotification) {
|
|
try {
|
|
onDesktopNotification(payload);
|
|
return true;
|
|
} catch {
|
|
// ignore host-side throw
|
|
}
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
// stdout fallback for runtimes that parse the one-line `${prefix}{json}` protocol.
|
|
process.stdout.write(`${desktopNotifyPrefix}${JSON.stringify(payload)}\n`);
|
|
return true;
|
|
} catch {
|
|
// ignore
|
|
}
|
|
|
|
return false;
|
|
};
|
|
|
|
const broadcastUiNotification = (payload, options = {}) => {
|
|
const desktopNotifyEnabled = getDesktopNotifyEnabled();
|
|
if (!payload || typeof payload !== 'object') {
|
|
return;
|
|
}
|
|
|
|
const desktopNotificationDelivered = options.desktopNotificationDelivered === true;
|
|
|
|
const syntheticPayload = {
|
|
type: 'openchamber:notification',
|
|
properties: {
|
|
...payload,
|
|
// Tell local desktop UI whether a native channel already accepted this
|
|
// notification. If so, the SSE/WS event is informational only and must
|
|
// not create a second OS notification.
|
|
desktopNotificationDelivered,
|
|
// Legacy marker retained for older clients that only know about stdout.
|
|
desktopStdoutActive: desktopNotifyEnabled,
|
|
},
|
|
};
|
|
|
|
const broadcastGlobalUiEvent = typeof getBroadcastGlobalUiEvent === 'function'
|
|
? getBroadcastGlobalUiEvent()
|
|
: null;
|
|
if (broadcastGlobalUiEvent) {
|
|
broadcastGlobalUiEvent(syntheticPayload);
|
|
return;
|
|
}
|
|
|
|
const clients = getUiNotificationClients();
|
|
if (clients.size === 0) {
|
|
return;
|
|
}
|
|
|
|
for (const res of clients) {
|
|
try {
|
|
writeSseEvent(res, syntheticPayload);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
};
|
|
|
|
return {
|
|
writeSseEvent,
|
|
emitDesktopNotification,
|
|
broadcastUiNotification,
|
|
setOnDesktopNotification,
|
|
};
|
|
};
|