Merge pull request #221 from gnanam1990/fix/keyboard-freeze-mcp-notifications

fix: prevent keyboard freeze when MCP notification effects fire
This commit is contained in:
Kevin Codex
2026-04-03 10:27:11 +08:00
committed by GitHub
4 changed files with 68 additions and 40 deletions

View File

@@ -1,6 +1,7 @@
import type * as React from 'react'; import type * as React from 'react';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { useAppStateStore, useSetAppState } from 'src/state/AppState.js'; import { useAppStateStore, useSetAppState } from 'src/state/AppState.js';
import { logError } from '../utils/log.js';
import type { Theme } from '../utils/theme.js'; import type { Theme } from '../utils/theme.js';
type Priority = 'low' | 'medium' | 'high' | 'immediate'; type Priority = 'low' | 'medium' | 'high' | 'immediate';
type BaseNotification = { type BaseNotification = {
@@ -44,6 +45,7 @@ export function useNotifications(): {
// Process queue when current notification finishes or queue changes // Process queue when current notification finishes or queue changes
const processQueue = useCallback(() => { const processQueue = useCallback(() => {
try {
setAppState(prev => { setAppState(prev => {
const next = getNext(prev.notifications.queue); const next = getNext(prev.notifications.queue);
if (prev.notifications.current !== null || !next) { if (prev.notifications.current !== null || !next) {
@@ -74,8 +76,12 @@ export function useNotifications(): {
} }
}; };
}); });
} catch (error) {
logError(error);
}
}, [setAppState]); }, [setAppState]);
const addNotification = useCallback<AddNotificationFn>((notif: Notification) => { const addNotification = useCallback<AddNotificationFn>((notif: Notification) => {
try {
// Handle immediate priority notifications // Handle immediate priority notifications
if (notif.priority === 'immediate') { if (notif.priority === 'immediate') {
// Clear any existing timeout since we're showing a new immediate notification // 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 // Process queue after adding the notification
processQueue(); processQueue();
} catch (error) {
logError(error);
}
}, [setAppState, processQueue]); }, [setAppState, processQueue]);
const removeNotification = useCallback<RemoveNotificationFn>((key: string) => { const removeNotification = useCallback<RemoveNotificationFn>((key: string) => {
try {
setAppState(prev => { setAppState(prev => {
const isCurrent = prev.notifications.current?.key === key; const isCurrent = prev.notifications.current?.key === key;
const inQueue = prev.notifications.queue.some(n => n.key === key); const inQueue = prev.notifications.queue.some(n => n.key === key);
@@ -210,6 +220,9 @@ export function useNotifications(): {
}; };
}); });
processQueue(); processQueue();
} catch (error) {
logError(error);
}
}, [setAppState, processQueue]); }, [setAppState, processQueue]);
// Process queue on mount if there are notifications in the initial state. // Process queue on mount if there are notifications in the initial state.

View File

@@ -1,5 +1,6 @@
import { c as _c } from "react-compiler-runtime"; import { c as _c } from "react-compiler-runtime";
import * as React from 'react'; import * as React from 'react';
import { logError } from '../../utils/log.js';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useNotifications } from 'src/context/notifications.js'; import { useNotifications } from 'src/context/notifications.js';
import { getIsRemoteMode } from '../../bootstrap/state.js'; import { getIsRemoteMode } from '../../bootstrap/state.js';
@@ -23,43 +24,47 @@ export function useMcpConnectivityStatus(t0) {
let t3; let t3;
if ($[0] !== addNotification || $[1] !== mcpClients) { if ($[0] !== addNotification || $[1] !== mcpClients) {
t2 = () => { t2 = () => {
if (getIsRemoteMode()) { try {
return; if (getIsRemoteMode()) {
} return;
const failedLocalClients = mcpClients.filter(_temp); }
const failedClaudeAiClients = mcpClients.filter(_temp2); const failedLocalClients = mcpClients.filter(_temp);
const needsAuthLocalServers = mcpClients.filter(_temp3); const failedClaudeAiClients = mcpClients.filter(_temp2);
const needsAuthClaudeAiServers = mcpClients.filter(_temp4); const needsAuthLocalServers = mcpClients.filter(_temp3);
if (failedLocalClients.length === 0 && failedClaudeAiClients.length === 0 && needsAuthLocalServers.length === 0 && needsAuthClaudeAiServers.length === 0) { const needsAuthClaudeAiServers = mcpClients.filter(_temp4);
return; if (failedLocalClients.length === 0 && failedClaudeAiClients.length === 0 && needsAuthLocalServers.length === 0 && needsAuthClaudeAiServers.length === 0) {
} return;
if (failedLocalClients.length > 0) { }
addNotification({ if (failedLocalClients.length > 0) {
key: "mcp-failed", addNotification({
jsx: <><Text color="error">{failedLocalClients.length} MCP{" "}{failedLocalClients.length === 1 ? "server" : "servers"} failed</Text><Text dimColor={true}> · /mcp</Text></>, key: "mcp-failed",
priority: "medium" jsx: <><Text color="error">{failedLocalClients.length} MCP{" "}{failedLocalClients.length === 1 ? "server" : "servers"} failed</Text><Text dimColor={true}> · /mcp</Text></>,
}); priority: "medium"
} });
if (failedClaudeAiClients.length > 0) { }
addNotification({ if (failedClaudeAiClients.length > 0) {
key: "mcp-claudeai-failed", addNotification({
jsx: <><Text color="error">{failedClaudeAiClients.length} claude.ai{" "}{failedClaudeAiClients.length === 1 ? "connector" : "connectors"}{" "}unavailable</Text><Text dimColor={true}> · /mcp</Text></>, key: "mcp-claudeai-failed",
priority: "medium" jsx: <><Text color="error">{failedClaudeAiClients.length} claude.ai{" "}{failedClaudeAiClients.length === 1 ? "connector" : "connectors"}{" "}unavailable</Text><Text dimColor={true}> · /mcp</Text></>,
}); priority: "medium"
} });
if (needsAuthLocalServers.length > 0) { }
addNotification({ if (needsAuthLocalServers.length > 0) {
key: "mcp-needs-auth", addNotification({
jsx: <><Text color="warning">{needsAuthLocalServers.length} MCP{" "}{needsAuthLocalServers.length === 1 ? "server needs" : "servers need"}{" "}auth</Text><Text dimColor={true}> · /mcp</Text></>, key: "mcp-needs-auth",
priority: "medium" jsx: <><Text color="warning">{needsAuthLocalServers.length} MCP{" "}{needsAuthLocalServers.length === 1 ? "server needs" : "servers need"}{" "}auth</Text><Text dimColor={true}> · /mcp</Text></>,
}); priority: "medium"
} });
if (needsAuthClaudeAiServers.length > 0) { }
addNotification({ if (needsAuthClaudeAiServers.length > 0) {
key: "mcp-claudeai-needs-auth", addNotification({
jsx: <><Text color="warning">{needsAuthClaudeAiServers.length} claude.ai{" "}{needsAuthClaudeAiServers.length === 1 ? "connector needs" : "connectors need"}{" "}auth</Text><Text dimColor={true}> · /mcp</Text></>, key: "mcp-claudeai-needs-auth",
priority: "medium" jsx: <><Text color="warning">{needsAuthClaudeAiServers.length} claude.ai{" "}{needsAuthClaudeAiServers.length === 1 ? "connector needs" : "connectors need"}{" "}auth</Text><Text dimColor={true}> · /mcp</Text></>,
}); priority: "medium"
});
}
} catch (error) {
logError(error);
} }
}; };
t3 = [addNotification, mcpClients]; t3 = [addNotification, mcpClients];

View File

@@ -433,6 +433,8 @@ const reconciler = createReconciler<
scheduleTimeout: setTimeout, scheduleTimeout: setTimeout,
cancelTimeout: clearTimeout, cancelTimeout: clearTimeout,
noTimeout: -1, noTimeout: -1,
supportsMicrotasks: true,
scheduleMicrotask: queueMicrotask,
getCurrentUpdatePriority: () => dispatcher.currentUpdatePriority, getCurrentUpdatePriority: () => dispatcher.currentUpdatePriority,
beforeActiveInstanceBlur() {}, beforeActiveInstanceBlur() {},
afterActiveInstanceBlur() {}, afterActiveInstanceBlur() {},

View File

@@ -12,9 +12,17 @@ const originalEnv = {
} }
afterEach(() => { afterEach(() => {
process.env.CLAUDE_CODE_USE_OPENAI = originalEnv.CLAUDE_CODE_USE_OPENAI if (originalEnv.CLAUDE_CODE_USE_OPENAI === undefined) {
process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = delete process.env.CLAUDE_CODE_USE_OPENAI
originalEnv.CLAUDE_CODE_MAX_OUTPUT_TOKENS } else {
process.env.CLAUDE_CODE_USE_OPENAI = originalEnv.CLAUDE_CODE_USE_OPENAI
}
if (originalEnv.CLAUDE_CODE_MAX_OUTPUT_TOKENS === undefined) {
delete process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS
} else {
process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS =
originalEnv.CLAUDE_CODE_MAX_OUTPUT_TOKENS
}
}) })
test('deepseek-chat uses provider-specific context and output caps', () => { test('deepseek-chat uses provider-specific context and output caps', () => {