fix: dedupe vscode notifications across panels
Prevents duplicate native notifications when a session is open in both sidebar and editor Uses an extension-host claim step so only one VS Code webview displays each notification Keeps notification behavior unchanged for single-panel sessions
This commit is contained in:
@@ -22,6 +22,26 @@ type SystemRuntimeDeps = {
|
|||||||
clientReloadDelayMs: number;
|
clientReloadDelayMs: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const NOTIFICATION_CLAIM_TTL_MS = 10_000;
|
||||||
|
const notificationClaims = new Map<string, number>();
|
||||||
|
|
||||||
|
const claimNotification = (key: string): boolean => {
|
||||||
|
const now = Date.now();
|
||||||
|
for (const [claimKey, claimedAt] of notificationClaims) {
|
||||||
|
if (now - claimedAt > NOTIFICATION_CLAIM_TTL_MS) {
|
||||||
|
notificationClaims.delete(claimKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = notificationClaims.get(key);
|
||||||
|
if (existing && now - existing <= NOTIFICATION_CLAIM_TTL_MS) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationClaims.set(key, now);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const getOpenChamberConfigDir = (): string => {
|
const getOpenChamberConfigDir = (): string => {
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
@@ -225,6 +245,13 @@ export async function handleSystemBridgeMessage(
|
|||||||
return { id, type, success: true, data: getSessionActivitySnapshot() };
|
return { id, type, success: true, data: getSessionActivitySnapshot() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'api:notifications:claim': {
|
||||||
|
const key = typeof (payload as { key?: unknown } | undefined)?.key === 'string'
|
||||||
|
? (payload as { key: string }).key.trim()
|
||||||
|
: '';
|
||||||
|
return { id, type, success: true, data: { claimed: key ? claimNotification(key) : false } };
|
||||||
|
}
|
||||||
|
|
||||||
case 'api:zen:models': {
|
case 'api:zen:models': {
|
||||||
const models = await fetchFreeZenModels();
|
const models = await fetchFreeZenModels();
|
||||||
return { id, type, success: true, data: { models } };
|
return { id, type, success: true, data: { models } };
|
||||||
|
|||||||
@@ -1279,12 +1279,32 @@ onCommand('showSettings', () => {
|
|||||||
window.dispatchEvent(new CustomEvent('openchamber:navigate', { detail: { view: 'settings' } }));
|
window.dispatchEvent(new CustomEvent('openchamber:navigate', { detail: { view: 'settings' } }));
|
||||||
});
|
});
|
||||||
|
|
||||||
const showOpenChamberNotification = (payload: { title?: unknown; body?: unknown; sessionId?: unknown; requireHidden?: unknown } | undefined) => {
|
const getNotificationClaimKey = (payload: { title?: unknown; body?: unknown; sessionId?: unknown; tag?: unknown } | undefined): string => {
|
||||||
|
const tag = typeof payload?.tag === 'string' ? payload.tag.trim() : '';
|
||||||
|
if (tag) return tag;
|
||||||
|
return [payload?.sessionId, payload?.title, payload?.body]
|
||||||
|
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
||||||
|
.map((value) => value.trim())
|
||||||
|
.join('|');
|
||||||
|
};
|
||||||
|
|
||||||
|
const claimOpenChamberNotification = async (payload: { title?: unknown; body?: unknown; sessionId?: unknown; tag?: unknown } | undefined): Promise<boolean> => {
|
||||||
|
const key = getNotificationClaimKey(payload);
|
||||||
|
if (!key) return true;
|
||||||
|
try {
|
||||||
|
const result = await sendBridgeMessage<{ claimed?: boolean }>('api:notifications:claim', { key });
|
||||||
|
return result?.claimed === true;
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const showOpenChamberNotification = (payload: { title?: unknown; body?: unknown; sessionId?: unknown; tag?: unknown; requireHidden?: unknown } | undefined) => {
|
||||||
if (typeof Notification === 'undefined') {
|
if (typeof Notification === 'undefined') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const show = () => {
|
const show = async () => {
|
||||||
const isVSCodeWindowFocused = window.__OPENCHAMBER_VSCODE_WINDOW_FOCUSED__ ?? document.hasFocus();
|
const isVSCodeWindowFocused = window.__OPENCHAMBER_VSCODE_WINDOW_FOCUSED__ ?? document.hasFocus();
|
||||||
if (payload?.requireHidden === true && isVSCodeWindowFocused) {
|
if (payload?.requireHidden === true && isVSCodeWindowFocused) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1300,6 +1320,9 @@ const showOpenChamberNotification = (payload: { title?: unknown; body?: unknown;
|
|||||||
const sessionId = typeof payload?.sessionId === 'string' && payload.sessionId.trim().length > 0
|
const sessionId = typeof payload?.sessionId === 'string' && payload.sessionId.trim().length > 0
|
||||||
? payload.sessionId.trim()
|
? payload.sessionId.trim()
|
||||||
: '';
|
: '';
|
||||||
|
if (!await claimOpenChamberNotification({ ...payload, title, body, sessionId })) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const notification = new Notification(title, { body });
|
const notification = new Notification(title, { body });
|
||||||
notification.onclick = () => {
|
notification.onclick = () => {
|
||||||
@@ -1316,13 +1339,14 @@ const showOpenChamberNotification = (payload: { title?: unknown; body?: unknown;
|
|||||||
if (Notification.permission === 'default') {
|
if (Notification.permission === 'default') {
|
||||||
void Notification.requestPermission().then((permission) => {
|
void Notification.requestPermission().then((permission) => {
|
||||||
if (permission === 'granted') {
|
if (permission === 'granted') {
|
||||||
show();
|
void show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return show();
|
void show();
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
onCommand('showNotification', (payload) => {
|
onCommand('showNotification', (payload) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user