feat(notifications): add overlay notifications with position config

- Add Catppuccin Macchiato overlay notification stack with 3s transient timeout
- Add `notifications.overlayPosition` config (top-left | top | top-right)
- Route startup tokenization and subtitle annotation status through configured surfaces
- Deduplicate rapid subtitle mode toggle notifications
- Change `both` to mean overlay + system; add `osd-system` as legacy alias for old behavior
- Keep `osd`/`osd-system` as config-file-only legacy values; Settings UI offers overlay/system/both/none
This commit is contained in:
2026-06-04 21:56:51 -07:00
parent 311f1e8ee5
commit 9247248d48
83 changed files with 2296 additions and 240 deletions
@@ -70,7 +70,7 @@ test('auto sync notifications send osd updates for progress phases', () => {
]);
});
test('auto sync notifications never send desktop notifications', () => {
test('auto sync notifications route both to overlay and system only', () => {
const calls: string[] = [];
notifyCharacterDictionaryAutoSyncStatus(makeEvent('syncing', 'syncing'), {
@@ -80,14 +80,10 @@ test('auto sync notifications never send desktop notifications', () => {
},
showDesktopNotification: (title, options) =>
calls.push(`desktop:${title}:${options.body ?? ''}`),
});
notifyCharacterDictionaryAutoSyncStatus(makeEvent('importing', 'importing'), {
getNotificationType: () => 'both',
showOsd: (message) => {
calls.push(`osd:${message}`);
},
showDesktopNotification: (title, options) =>
calls.push(`desktop:${title}:${options.body ?? ''}`),
showOverlayNotification: (payload) =>
calls.push(
`overlay:${payload.id}:${payload.title}:${payload.body}:${payload.persistent ? 'pin' : 'auto'}`,
),
});
notifyCharacterDictionaryAutoSyncStatus(makeEvent('ready', 'ready'), {
getNotificationType: () => 'both',
@@ -96,9 +92,25 @@ test('auto sync notifications never send desktop notifications', () => {
},
showDesktopNotification: (title, options) =>
calls.push(`desktop:${title}:${options.body ?? ''}`),
showOverlayNotification: (payload) =>
calls.push(
`overlay:${payload.id}:${payload.title}:${payload.body}:${payload.persistent ? 'pin' : 'auto'}`,
),
});
notifyCharacterDictionaryAutoSyncStatus(makeEvent('failed', 'failed'), {
getNotificationType: () => 'both',
assert.deepEqual(calls, [
'overlay:character-dictionary-auto-sync:Character dictionary:syncing:pin',
'desktop:SubMiner:syncing',
'overlay:character-dictionary-auto-sync:Character dictionary:ready:auto',
'desktop:SubMiner:ready',
]);
});
test('auto sync notifications fall back to desktop when overlay routing is unavailable', () => {
const calls: string[] = [];
notifyCharacterDictionaryAutoSyncStatus(makeEvent('building', 'building'), {
getNotificationType: () => undefined,
showOsd: (message) => {
calls.push(`osd:${message}`);
},
@@ -106,14 +118,30 @@ test('auto sync notifications never send desktop notifications', () => {
calls.push(`desktop:${title}:${options.body ?? ''}`),
});
assert.deepEqual(calls, ['osd:syncing', 'osd:importing', 'osd:ready', 'osd:failed']);
assert.deepEqual(calls, ['desktop:SubMiner:building']);
});
test('auto sync notifications fall back to desktop for long progress when osd is unavailable', () => {
test('auto sync notifications keep osd-system on legacy surfaces', () => {
const calls: string[] = [];
notifyCharacterDictionaryAutoSyncStatus(makeEvent('syncing', 'syncing'), {
getNotificationType: () => 'osd-system',
showOsd: (message) => {
calls.push(`osd:${message}`);
},
showDesktopNotification: (title, options) =>
calls.push(`desktop:${title}:${options.body ?? ''}`),
showOverlayNotification: (payload) => calls.push(`overlay:${payload.body}`),
});
assert.deepEqual(calls, ['osd:syncing', 'desktop:SubMiner:syncing']);
});
test('auto sync notifications keep osd-system desktop delivery even when osd is unavailable', () => {
const calls: string[] = [];
notifyCharacterDictionaryAutoSyncStatus(makeEvent('generating', 'generating'), {
getNotificationType: () => 'both',
getNotificationType: () => 'osd-system',
showOsd: (message) => {
calls.push(`osd:${message}`);
return false;
@@ -122,7 +150,7 @@ test('auto sync notifications fall back to desktop for long progress when osd is
calls.push(`desktop:${title}:${options.body ?? ''}`),
});
notifyCharacterDictionaryAutoSyncStatus(makeEvent('ready', 'ready'), {
getNotificationType: () => 'both',
getNotificationType: () => 'osd-system',
showOsd: (message) => {
calls.push(`osd:${message}`);
return false;
@@ -131,14 +159,19 @@ test('auto sync notifications fall back to desktop for long progress when osd is
calls.push(`desktop:${title}:${options.body ?? ''}`),
});
assert.deepEqual(calls, ['osd:generating', 'desktop:SubMiner:generating', 'osd:ready']);
assert.deepEqual(calls, [
'osd:generating',
'desktop:SubMiner:generating',
'osd:ready',
'desktop:SubMiner:ready',
]);
});
test('auto sync notifications fall back to desktop when startup sequencer cannot show osd', () => {
test('auto sync notifications send osd-system desktop updates with startup sequencer', () => {
const calls: string[] = [];
notifyCharacterDictionaryAutoSyncStatus(makeEvent('importing', 'importing'), {
getNotificationType: () => 'both',
getNotificationType: () => 'osd-system',
showOsd: (message) => {
calls.push(`osd:${message}`);
},