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 501304e451
commit d033884b09
68 changed files with 1408 additions and 69 deletions
+69
View File
@@ -2,6 +2,10 @@ import test from 'node:test';
import assert from 'node:assert/strict';
import { AppReadyRuntimeDeps, runAppReadyRuntime } from './startup';
function waitTurn(): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, 0));
}
function makeDeps(overrides: Partial<AppReadyRuntimeDeps> = {}) {
const calls: string[] = [];
const deps = {
@@ -277,6 +281,71 @@ test('runAppReadyRuntime does not await background warmups', async () => {
releaseWarmup();
});
test('runAppReadyRuntime handles managed background initial args before deferred Yomitan wait', async () => {
const calls: string[] = [];
let releaseYomitan!: () => void;
const yomitanGate = new Promise<void>((resolve) => {
releaseYomitan = resolve;
});
const { deps } = makeDeps({
shouldAutoInitializeOverlayRuntimeFromConfig: () => false,
shouldHandleInitialArgsBeforeDeferredOverlayWarmup: () => true,
loadYomitanExtension: async () => {
calls.push('loadYomitanExtension:start');
await yomitanGate;
calls.push('loadYomitanExtension:done');
},
handleFirstRunSetup: async () => {
calls.push('handleFirstRunSetup');
},
handleInitialArgs: () => {
calls.push('handleInitialArgs');
},
} as Partial<AppReadyRuntimeDeps>);
const readyPromise = runAppReadyRuntime(deps);
await waitTurn();
try {
assert.ok(calls.includes('handleFirstRunSetup'));
assert.ok(calls.includes('handleInitialArgs'));
assert.equal(calls.includes('loadYomitanExtension:done'), false);
} finally {
releaseYomitan();
await readyPromise;
}
});
test('runAppReadyRuntime keeps non-managed deferred overlay startup behind Yomitan readiness', async () => {
const calls: string[] = [];
let releaseYomitan!: () => void;
const yomitanGate = new Promise<void>((resolve) => {
releaseYomitan = resolve;
});
const { deps } = makeDeps({
shouldAutoInitializeOverlayRuntimeFromConfig: () => false,
shouldHandleInitialArgsBeforeDeferredOverlayWarmup: () => false,
loadYomitanExtension: async () => {
calls.push('loadYomitanExtension:start');
await yomitanGate;
calls.push('loadYomitanExtension:done');
},
handleInitialArgs: () => {
calls.push('handleInitialArgs');
},
} as Partial<AppReadyRuntimeDeps>);
const readyPromise = runAppReadyRuntime(deps);
await waitTurn();
assert.equal(calls.includes('handleInitialArgs'), false);
releaseYomitan();
await readyPromise;
assert.ok(calls.indexOf('loadYomitanExtension:done') < calls.indexOf('handleInitialArgs'));
});
test('runAppReadyRuntime starts background warmups before core runtime services', async () => {
const calls: string[] = [];
const { deps } = makeDeps({