feat(notifications): add notification history panel and overlay UX fixes

- New toggleNotificationHistory (Ctrl+N) session-scoped history panel; slides in from same edge as notification stack
- Overlay error/recovery toast follows notifications.overlayPosition; stack and history side seeded at startup
- Cold managed background startup initializes tray and visible overlay shell before tokenization warmups finish
- Add Update button to overlay update-available notifications
- Fix Ctrl+S sentence-card flow: only Anki progress notification, no duplicate status toast
- Fix overlay notification close/actions clickability above subtitle bars on Linux
- Increase pause-until-ready default timeout from 15s to 30s
This commit is contained in:
2026-06-06 15:29:14 -07:00
parent a34ec049a2
commit 8111deac44
68 changed files with 1408 additions and 69 deletions
+2
View File
@@ -62,6 +62,7 @@ export const IPC_CHANNELS = {
getConfigShortcuts: 'get-config-shortcuts',
getStatsToggleKey: 'get-stats-toggle-key',
getMarkWatchedKey: 'get-mark-watched-key',
getOverlayNotificationPosition: 'get-overlay-notification-position',
getControllerConfig: 'get-controller-config',
getSecondarySubMode: 'get-secondary-sub-mode',
getCurrentSecondarySub: 'get-current-secondary-sub',
@@ -146,6 +147,7 @@ export const IPC_CHANNELS = {
primarySubtitleBarToggle: 'primary-subtitle-bar:toggle',
configHotReload: 'config:hot-reload',
overlayNotification: 'overlay:notification',
notificationHistoryToggle: 'notification-history:toggle',
},
} as const;
+44
View File
@@ -0,0 +1,44 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { DEFAULT_CONFIG, DEFAULT_KEYBINDINGS } from '../../config/definitions';
import { compileSessionBindings } from '../../core/services/session-bindings';
import { resolveConfiguredShortcuts } from '../../core/utils/shortcut-config';
import { parseSessionActionDispatchRequest } from './validators';
// Regression guard: SESSION_ACTION_IDS in validators.ts is a hand-maintained mirror of the
// SessionActionId union. If a new shortcut-backed action is added to the union/defaults but not to
// the validator allow-list, the renderer's dispatchSessionAction IPC is rejected at runtime (which
// surfaces as a "Renderer error recovered" toast). Compile every default binding and assert the
// validator accepts each one so the two lists can't silently drift apart.
test('every default session-action binding is accepted by parseSessionActionDispatchRequest', () => {
const { bindings } = compileSessionBindings({
shortcuts: resolveConfiguredShortcuts(DEFAULT_CONFIG, DEFAULT_CONFIG),
keybindings: DEFAULT_KEYBINDINGS,
statsToggleKey: DEFAULT_CONFIG.stats.toggleKey,
statsMarkWatchedKey: DEFAULT_CONFIG.stats.markWatchedKey,
platform: 'linux',
rawConfig: DEFAULT_CONFIG,
});
const sessionActions = bindings.filter((binding) => binding.actionType === 'session-action');
assert.ok(sessionActions.length > 0, 'expected default session-action bindings to exist');
for (const binding of sessionActions) {
if (binding.actionType !== 'session-action') continue;
const request =
binding.payload === undefined
? { actionId: binding.actionId }
: { actionId: binding.actionId, payload: binding.payload };
assert.ok(
parseSessionActionDispatchRequest(request) !== null,
`validator rejected session action: ${binding.actionId}`,
);
}
});
test('toggleNotificationHistory dispatch request is accepted', () => {
assert.deepEqual(parseSessionActionDispatchRequest({ actionId: 'toggleNotificationHistory' }), {
actionId: 'toggleNotificationHistory',
});
});
+2
View File
@@ -20,6 +20,7 @@ const RESERVED_CONTROLLER_PROFILE_IDS = new Set(['__proto__', 'constructor', 'pr
const SESSION_ACTION_IDS: SessionActionId[] = [
'toggleStatsOverlay',
'markWatched',
'toggleVisibleOverlay',
'copySubtitle',
'copySubtitleMultiple',
@@ -31,6 +32,7 @@ const SESSION_ACTION_IDS: SessionActionId[] = [
'toggleSecondarySub',
'markAudioCard',
'toggleSubtitleSidebar',
'toggleNotificationHistory',
'openRuntimeOptions',
'openSessionHelp',
'openCharacterDictionaryManager',
@@ -14,6 +14,8 @@ export interface SubminerPluginRuntimeScriptOptConfig {
texthookerEnabled: boolean;
}
const AUTO_START_PAUSE_UNTIL_READY_TIMEOUT_SECONDS = 30;
function boolScriptOpt(value: boolean): 'yes' | 'no' {
return value ? 'yes' : 'no';
}
@@ -42,6 +44,7 @@ export function buildSubminerPluginRuntimeScriptOptParts(
`subminer-auto_start_pause_until_ready=${boolScriptOpt(
runtimeConfig.autoStartPauseUntilReady,
)}`,
`subminer-auto_start_pause_until_ready_timeout_seconds=${AUTO_START_PAUSE_UNTIL_READY_TIMEOUT_SECONDS}`,
`subminer-osd_messages=${boolScriptOpt(runtimeConfig.osdMessages)}`,
`subminer-texthooker_enabled=${boolScriptOpt(runtimeConfig.texthookerEnabled)}`,
];