fix: prevent keyboard freeze when MCP notification effects fire
React 19 requires `supportsMicrotasks: true` in the reconciler host config so it can flush state updates from passive effects via queueMicrotask. Without this, state updates triggered inside useMcpConnectivityStatus were silently dropped, corrupting React's internal executionContext and causing all keyboard input to freeze after the "N MCP server(s) need auth" notification appeared. Root cause (three-part fix): 1. reconciler.ts: declare supportsMicrotasks + scheduleMicrotask so React 19 schedules passive-effect flushes correctly. 2. useMcpConnectivityStatus.tsx: wrap the MCP auth notification effect in try/catch so any unexpected throw does not propagate into flushPassiveEffects and permanently corrupt executionContext. 3. notifications.tsx: wrap addNotification, removeNotification, and processQueue in try/catch for the same reason — these are called from 12+ notification hooks across passive effects. Also fixes a pre-existing test isolation bug in context.test.ts where assigning `undefined` to process.env produced the string "undefined", polluting the env for subsequent test files. Resolves: #169, #205, #77
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import type * as React from 'react';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useAppStateStore, useSetAppState } from 'src/state/AppState.js';
|
||||
import { logError } from '../utils/log.js';
|
||||
import type { Theme } from '../utils/theme.js';
|
||||
type Priority = 'low' | 'medium' | 'high' | 'immediate';
|
||||
type BaseNotification = {
|
||||
@@ -44,6 +45,7 @@ export function useNotifications(): {
|
||||
|
||||
// Process queue when current notification finishes or queue changes
|
||||
const processQueue = useCallback(() => {
|
||||
try {
|
||||
setAppState(prev => {
|
||||
const next = getNext(prev.notifications.queue);
|
||||
if (prev.notifications.current !== null || !next) {
|
||||
@@ -74,8 +76,12 @@ export function useNotifications(): {
|
||||
}
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
}
|
||||
}, [setAppState]);
|
||||
const addNotification = useCallback<AddNotificationFn>((notif: Notification) => {
|
||||
try {
|
||||
// Handle immediate priority notifications
|
||||
if (notif.priority === 'immediate') {
|
||||
// Clear any existing timeout since we're showing a new immediate notification
|
||||
@@ -189,8 +195,12 @@ export function useNotifications(): {
|
||||
|
||||
// Process queue after adding the notification
|
||||
processQueue();
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
}
|
||||
}, [setAppState, processQueue]);
|
||||
const removeNotification = useCallback<RemoveNotificationFn>((key: string) => {
|
||||
try {
|
||||
setAppState(prev => {
|
||||
const isCurrent = prev.notifications.current?.key === key;
|
||||
const inQueue = prev.notifications.queue.some(n => n.key === key);
|
||||
@@ -210,6 +220,9 @@ export function useNotifications(): {
|
||||
};
|
||||
});
|
||||
processQueue();
|
||||
} catch (error) {
|
||||
logError(error);
|
||||
}
|
||||
}, [setAppState, processQueue]);
|
||||
|
||||
// Process queue on mount if there are notifications in the initial state.
|
||||
|
||||
Reference in New Issue
Block a user