feat: add notifyOnSubtasks setting to control subtask notifications (#227)

* feat: add notifyOnSubtasks setting to control subtask notifications

- Add notifyOnSubtasks toggle in NotificationSettings (moved to general section)
- Update useEventStream.ts to skip notifications for subtasks when disabled
- Add server-side push notification control for subtasks
- Update UI store, persistence, and desktop settings types

* feat: apply notifyOnSubtasks across web push + desktop notifications

---------

Co-authored-by: Jovines <jovines@qq.com>
This commit is contained in:
Jovines
2026-01-30 14:15:00 +02:00
committed by GitHub
co-authored by Jovines
parent 344f369093
commit de468361ec
10 changed files with 354 additions and 93 deletions
+66
View File
@@ -783,6 +783,9 @@ const sanitizeSettingsUpdate = (payload) => {
result.notificationMode = mode;
}
}
if (typeof candidate.notifyOnSubtasks === 'boolean') {
result.notifyOnSubtasks = candidate.notifyOnSubtasks;
}
if (typeof candidate.autoDeleteEnabled === 'boolean') {
result.autoDeleteEnabled = candidate.autoDeleteEnabled;
}
@@ -1861,6 +1864,53 @@ const pushPermissionDebounceTimers = new Map();
const notifiedPermissionRequests = new Set();
const lastReadyNotificationAt = new Map();
// Cache: sessionId -> parentID (string) or null (no parent). Undefined = unknown.
const sessionParentIdCache = new Map();
const SESSION_PARENT_CACHE_TTL_MS = 60 * 1000;
const getCachedSessionParentId = (sessionId) => {
const entry = sessionParentIdCache.get(sessionId);
if (!entry) return undefined;
if (Date.now() - entry.at > SESSION_PARENT_CACHE_TTL_MS) {
sessionParentIdCache.delete(sessionId);
return undefined;
}
return entry.parentID;
};
const setCachedSessionParentId = (sessionId, parentID) => {
sessionParentIdCache.set(sessionId, { parentID: parentID ?? null, at: Date.now() });
};
const fetchSessionParentId = async (sessionId) => {
if (!sessionId) return undefined;
const cached = getCachedSessionParentId(sessionId);
if (cached !== undefined) return cached;
try {
const response = await fetch(buildOpenCodeUrl('/session', ''), {
method: 'GET',
headers: { Accept: 'application/json' },
signal: AbortSignal.timeout(2000),
});
if (!response.ok) {
return undefined;
}
const data = await response.json().catch(() => null);
if (!Array.isArray(data)) {
return undefined;
}
const match = data.find((s) => s && typeof s === 'object' && s.id === sessionId);
const parentID = match && typeof match.parentID === 'string' && match.parentID.length > 0 ? match.parentID : null;
setCachedSessionParentId(sessionId, parentID);
return parentID;
} catch {
return undefined;
}
};
const extractSessionIdFromPayload = (payload) => {
if (!payload || typeof payload !== 'object') return null;
const props = payload.properties;
@@ -1919,6 +1969,22 @@ const maybeSendPushForTrigger = async (payload) => {
if (payload.type === 'message.updated') {
const info = payload.properties?.info;
if (info?.role === 'assistant' && info?.finish === 'stop' && sessionId) {
// Check if this is a subtask and if we should notify for subtasks
const settings = await readSettingsFromDisk();
if (settings.notifyOnSubtasks === false) {
// Prefer parentID on payload (if present), else fetch from sessions list.
const sessionInfo = payload.properties?.session;
const parentIDFromPayload = sessionInfo?.parentID ?? payload.properties?.parentID;
const parentID = parentIDFromPayload
? parentIDFromPayload
: await fetchSessionParentId(sessionId);
// Fail open: if parentID cannot be resolved, send notification.
if (parentID) {
return;
}
}
const now = Date.now();
const lastAt = lastReadyNotificationAt.get(sessionId) ?? 0;
if (now - lastAt < PUSH_READY_COOLDOWN_MS) {