diff --git a/src/context/notifications.tsx b/src/context/notifications.tsx index 8c214db5..adf34063 100644 --- a/src/context/notifications.tsx +++ b/src/context/notifications.tsx @@ -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((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((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. diff --git a/src/hooks/notifs/useMcpConnectivityStatus.tsx b/src/hooks/notifs/useMcpConnectivityStatus.tsx index cfcb9a7e..d5176e08 100644 --- a/src/hooks/notifs/useMcpConnectivityStatus.tsx +++ b/src/hooks/notifs/useMcpConnectivityStatus.tsx @@ -1,5 +1,6 @@ import { c as _c } from "react-compiler-runtime"; import * as React from 'react'; +import { logError } from '../../utils/log.js'; import { useEffect } from 'react'; import { useNotifications } from 'src/context/notifications.js'; import { getIsRemoteMode } from '../../bootstrap/state.js'; @@ -23,43 +24,47 @@ export function useMcpConnectivityStatus(t0) { let t3; if ($[0] !== addNotification || $[1] !== mcpClients) { t2 = () => { - if (getIsRemoteMode()) { - return; - } - const failedLocalClients = mcpClients.filter(_temp); - const failedClaudeAiClients = mcpClients.filter(_temp2); - const needsAuthLocalServers = mcpClients.filter(_temp3); - const needsAuthClaudeAiServers = mcpClients.filter(_temp4); - if (failedLocalClients.length === 0 && failedClaudeAiClients.length === 0 && needsAuthLocalServers.length === 0 && needsAuthClaudeAiServers.length === 0) { - return; - } - if (failedLocalClients.length > 0) { - addNotification({ - key: "mcp-failed", - jsx: <>{failedLocalClients.length} MCP{" "}{failedLocalClients.length === 1 ? "server" : "servers"} failed · /mcp, - priority: "medium" - }); - } - if (failedClaudeAiClients.length > 0) { - addNotification({ - key: "mcp-claudeai-failed", - jsx: <>{failedClaudeAiClients.length} claude.ai{" "}{failedClaudeAiClients.length === 1 ? "connector" : "connectors"}{" "}unavailable · /mcp, - priority: "medium" - }); - } - if (needsAuthLocalServers.length > 0) { - addNotification({ - key: "mcp-needs-auth", - jsx: <>{needsAuthLocalServers.length} MCP{" "}{needsAuthLocalServers.length === 1 ? "server needs" : "servers need"}{" "}auth · /mcp, - priority: "medium" - }); - } - if (needsAuthClaudeAiServers.length > 0) { - addNotification({ - key: "mcp-claudeai-needs-auth", - jsx: <>{needsAuthClaudeAiServers.length} claude.ai{" "}{needsAuthClaudeAiServers.length === 1 ? "connector needs" : "connectors need"}{" "}auth · /mcp, - priority: "medium" - }); + try { + if (getIsRemoteMode()) { + return; + } + const failedLocalClients = mcpClients.filter(_temp); + const failedClaudeAiClients = mcpClients.filter(_temp2); + const needsAuthLocalServers = mcpClients.filter(_temp3); + const needsAuthClaudeAiServers = mcpClients.filter(_temp4); + if (failedLocalClients.length === 0 && failedClaudeAiClients.length === 0 && needsAuthLocalServers.length === 0 && needsAuthClaudeAiServers.length === 0) { + return; + } + if (failedLocalClients.length > 0) { + addNotification({ + key: "mcp-failed", + jsx: <>{failedLocalClients.length} MCP{" "}{failedLocalClients.length === 1 ? "server" : "servers"} failed · /mcp, + priority: "medium" + }); + } + if (failedClaudeAiClients.length > 0) { + addNotification({ + key: "mcp-claudeai-failed", + jsx: <>{failedClaudeAiClients.length} claude.ai{" "}{failedClaudeAiClients.length === 1 ? "connector" : "connectors"}{" "}unavailable · /mcp, + priority: "medium" + }); + } + if (needsAuthLocalServers.length > 0) { + addNotification({ + key: "mcp-needs-auth", + jsx: <>{needsAuthLocalServers.length} MCP{" "}{needsAuthLocalServers.length === 1 ? "server needs" : "servers need"}{" "}auth · /mcp, + priority: "medium" + }); + } + if (needsAuthClaudeAiServers.length > 0) { + addNotification({ + key: "mcp-claudeai-needs-auth", + jsx: <>{needsAuthClaudeAiServers.length} claude.ai{" "}{needsAuthClaudeAiServers.length === 1 ? "connector needs" : "connectors need"}{" "}auth · /mcp, + priority: "medium" + }); + } + } catch (error) { + logError(error); } }; t3 = [addNotification, mcpClients]; diff --git a/src/ink/reconciler.ts b/src/ink/reconciler.ts index ba5e1395..53a292fb 100644 --- a/src/ink/reconciler.ts +++ b/src/ink/reconciler.ts @@ -433,6 +433,8 @@ const reconciler = createReconciler< scheduleTimeout: setTimeout, cancelTimeout: clearTimeout, noTimeout: -1, + supportsMicrotasks: true, + scheduleMicrotask: queueMicrotask, getCurrentUpdatePriority: () => dispatcher.currentUpdatePriority, beforeActiveInstanceBlur() {}, afterActiveInstanceBlur() {}, diff --git a/src/utils/context.test.ts b/src/utils/context.test.ts index a95b209e..f6bc80d3 100644 --- a/src/utils/context.test.ts +++ b/src/utils/context.test.ts @@ -12,9 +12,17 @@ const originalEnv = { } afterEach(() => { - process.env.CLAUDE_CODE_USE_OPENAI = originalEnv.CLAUDE_CODE_USE_OPENAI - process.env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = - originalEnv.CLAUDE_CODE_MAX_OUTPUT_TOKENS + if (originalEnv.CLAUDE_CODE_USE_OPENAI === undefined) { + delete process.env.CLAUDE_CODE_USE_OPENAI + } 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', () => {