style: 完成所有文件的lint

This commit is contained in:
claude-code-best
2026-05-01 21:39:30 +08:00
parent d136872cc9
commit 6182015005
1333 changed files with 68255 additions and 77882 deletions

View File

@@ -1,12 +1,12 @@
import * as React from 'react'
import { getOauthProfileFromApiKey } from 'src/services/oauth/getOauthProfile.js'
import { isClaudeAISubscriber } from 'src/utils/auth.js'
import { Text } from '@anthropic/ink'
import { logEvent } from '../../services/analytics/index.js'
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
import { useStartupNotification } from './useStartupNotification.js'
import * as React from 'react';
import { getOauthProfileFromApiKey } from 'src/services/oauth/getOauthProfile.js';
import { isClaudeAISubscriber } from 'src/utils/auth.js';
import { Text } from '@anthropic/ink';
import { logEvent } from '../../services/analytics/index.js';
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js';
import { useStartupNotification } from './useStartupNotification.js';
const MAX_SHOW_COUNT = 3
const MAX_SHOW_COUNT = 3;
/**
* Hook to check if the user has a subscription on Console but isn't logged into it.
@@ -14,16 +14,16 @@ const MAX_SHOW_COUNT = 3
export function useCanSwitchToExistingSubscription(): void {
useStartupNotification(async () => {
if ((getGlobalConfig().subscriptionNoticeCount ?? 0) >= MAX_SHOW_COUNT) {
return null
return null;
}
const subscriptionType = await getExistingClaudeSubscription()
if (subscriptionType === null) return null
const subscriptionType = await getExistingClaudeSubscription();
if (subscriptionType === null) return null;
saveGlobalConfig(current => ({
...current,
subscriptionNoticeCount: (current.subscriptionNoticeCount ?? 0) + 1,
}))
logEvent('tengu_switch_to_subscription_notice_shown', {})
}));
logEvent('tengu_switch_to_subscription_notice_shown', {});
return {
key: 'switch-to-subscription',
@@ -37,8 +37,8 @@ export function useCanSwitchToExistingSubscription(): void {
</Text>
),
priority: 'low',
}
})
};
});
}
/**
@@ -48,20 +48,20 @@ export function useCanSwitchToExistingSubscription(): void {
async function getExistingClaudeSubscription(): Promise<'Max' | 'Pro' | null> {
// If already using subscription auth, there is nothing to switch to
if (isClaudeAISubscriber()) {
return null
return null;
}
const profile = await getOauthProfileFromApiKey()
const profile = await getOauthProfileFromApiKey();
if (!profile) {
return null
return null;
}
if (profile.account.has_claude_max) {
return 'Max'
return 'Max';
}
if (profile.account.has_claude_pro) {
return 'Pro'
return 'Pro';
}
return null
return null;
}

View File

@@ -1,30 +1,30 @@
import { useEffect, useRef } from 'react'
import { useNotifications } from 'src/context/notifications.js'
import { getModelDeprecationWarning } from 'src/utils/model/deprecation.js'
import { getIsRemoteMode } from '../../bootstrap/state.js'
import { useEffect, useRef } from 'react';
import { useNotifications } from 'src/context/notifications.js';
import { getModelDeprecationWarning } from 'src/utils/model/deprecation.js';
import { getIsRemoteMode } from '../../bootstrap/state.js';
export function useDeprecationWarningNotification(model: string): void {
const { addNotification } = useNotifications()
const lastWarningRef = useRef<string | null>(null)
const { addNotification } = useNotifications();
const lastWarningRef = useRef<string | null>(null);
useEffect(() => {
if (getIsRemoteMode()) return
const deprecationWarning = getModelDeprecationWarning(model)
if (getIsRemoteMode()) return;
const deprecationWarning = getModelDeprecationWarning(model);
// Show warning if model is deprecated and we haven't shown this exact warning yet
if (deprecationWarning && deprecationWarning !== lastWarningRef.current) {
lastWarningRef.current = deprecationWarning
lastWarningRef.current = deprecationWarning;
addNotification({
key: 'model-deprecation-warning',
text: deprecationWarning,
color: 'warning',
priority: 'high',
})
});
}
// Reset tracking if model changes to non-deprecated
if (!deprecationWarning) {
lastWarningRef.current = null
lastWarningRef.current = null;
}
}, [model, addNotification])
}, [model, addNotification]);
}

View File

@@ -1,6 +1,6 @@
import { useEffect } from 'react'
import { useNotifications } from 'src/context/notifications.js'
import { useAppState, useSetAppState } from 'src/state/AppState.js'
import { useEffect } from 'react';
import { useNotifications } from 'src/context/notifications.js';
import { useAppState, useSetAppState } from 'src/state/AppState.js';
import {
type CooldownReason,
isFastModeEnabled,
@@ -8,25 +8,25 @@ import {
onCooldownTriggered,
onFastModeOverageRejection,
onOrgFastModeChanged,
} from 'src/utils/fastMode.js'
import { formatDuration } from 'src/utils/format.js'
import { getIsRemoteMode } from '../../bootstrap/state.js'
} from 'src/utils/fastMode.js';
import { formatDuration } from 'src/utils/format.js';
import { getIsRemoteMode } from '../../bootstrap/state.js';
const COOLDOWN_STARTED_KEY = 'fast-mode-cooldown-started'
const COOLDOWN_EXPIRED_KEY = 'fast-mode-cooldown-expired'
const ORG_CHANGED_KEY = 'fast-mode-org-changed'
const OVERAGE_REJECTED_KEY = 'fast-mode-overage-rejected'
const COOLDOWN_STARTED_KEY = 'fast-mode-cooldown-started';
const COOLDOWN_EXPIRED_KEY = 'fast-mode-cooldown-expired';
const ORG_CHANGED_KEY = 'fast-mode-org-changed';
const OVERAGE_REJECTED_KEY = 'fast-mode-overage-rejected';
export function useFastModeNotification(): void {
const { addNotification } = useNotifications()
const isFastMode = useAppState(s => s.fastMode)
const setAppState = useSetAppState()
const { addNotification } = useNotifications();
const isFastMode = useAppState(s => s.fastMode);
const setAppState = useSetAppState();
// Notify when org fast mode status changes
useEffect(() => {
if (getIsRemoteMode()) return
if (getIsRemoteMode()) return;
if (!isFastModeEnabled()) {
return
return;
}
return onOrgFastModeChanged(orgEnabled => {
@@ -36,55 +36,55 @@ export function useFastModeNotification(): void {
color: 'fastMode',
priority: 'immediate',
text: 'Fast mode is now available · /fast to turn on',
})
});
} else if (isFastMode) {
// Org disabled fast mode — permanently turn off fast mode
setAppState(prev => ({ ...prev, fastMode: false }))
setAppState(prev => ({ ...prev, fastMode: false }));
addNotification({
key: ORG_CHANGED_KEY,
color: 'warning',
priority: 'immediate',
text: 'Fast mode has been disabled by your organization',
})
});
}
})
}, [addNotification, isFastMode, setAppState])
});
}, [addNotification, isFastMode, setAppState]);
// Notify when fast mode is rejected due to overage/extra usage issues
useEffect(() => {
if (getIsRemoteMode()) return
if (!isFastModeEnabled()) return
if (getIsRemoteMode()) return;
if (!isFastModeEnabled()) return;
return onFastModeOverageRejection(message => {
setAppState(prev => ({ ...prev, fastMode: false }))
setAppState(prev => ({ ...prev, fastMode: false }));
addNotification({
key: OVERAGE_REJECTED_KEY,
color: 'warning',
priority: 'immediate',
text: message,
})
})
}, [addNotification, setAppState])
});
});
}, [addNotification, setAppState]);
useEffect(() => {
if (getIsRemoteMode()) return
if (getIsRemoteMode()) return;
if (!isFastMode) {
return
return;
}
const unsubTriggered = onCooldownTriggered((resetAt, reason) => {
const resetIn = formatDuration(resetAt - Date.now(), {
hideTrailingZeros: true,
})
const message = getCooldownMessage(reason, resetIn)
});
const message = getCooldownMessage(reason, resetIn);
addNotification({
key: COOLDOWN_STARTED_KEY,
invalidates: [COOLDOWN_EXPIRED_KEY],
text: message,
color: 'warning',
priority: 'immediate',
})
})
});
});
const unsubExpired = onCooldownExpired(() => {
addNotification({
key: COOLDOWN_EXPIRED_KEY,
@@ -92,20 +92,20 @@ export function useFastModeNotification(): void {
color: 'fastMode',
text: `Fast limit reset · now using fast mode`,
priority: 'immediate',
})
})
});
});
return () => {
unsubTriggered()
unsubExpired()
}
}, [addNotification, isFastMode])
unsubTriggered();
unsubExpired();
};
}, [addNotification, isFastMode]);
}
function getCooldownMessage(reason: CooldownReason, resetIn: string): string {
switch (reason) {
case 'overloaded':
return `Fast mode overloaded and is temporarily unavailable · resets in ${resetIn}`
return `Fast mode overloaded and is temporarily unavailable · resets in ${resetIn}`;
case 'rate_limit':
return `Fast limit reached and temporarily disabled · resets in ${resetIn}`
return `Fast limit reached and temporarily disabled · resets in ${resetIn}`;
}
}

View File

@@ -1,87 +1,63 @@
import React, { useEffect, useRef } from 'react'
import { useNotifications } from 'src/context/notifications.js'
import { Text } from '@anthropic/ink'
import type { MCPServerConnection } from 'src/services/mcp/types.js'
import { getGlobalConfig, saveGlobalConfig } from 'src/utils/config.js'
import {
detectIDEs,
type IDEExtensionInstallationStatus,
isJetBrainsIde,
isSupportedTerminal,
} from 'src/utils/ide.js'
import { getIsRemoteMode } from '../../bootstrap/state.js'
import { useIdeConnectionStatus } from '../useIdeConnectionStatus.js'
import type { IDESelection } from '../useIdeSelection.js'
import React, { useEffect, useRef } from 'react';
import { useNotifications } from 'src/context/notifications.js';
import { Text } from '@anthropic/ink';
import type { MCPServerConnection } from 'src/services/mcp/types.js';
import { getGlobalConfig, saveGlobalConfig } from 'src/utils/config.js';
import { detectIDEs, type IDEExtensionInstallationStatus, isJetBrainsIde, isSupportedTerminal } from 'src/utils/ide.js';
import { getIsRemoteMode } from '../../bootstrap/state.js';
import { useIdeConnectionStatus } from '../useIdeConnectionStatus.js';
import type { IDESelection } from '../useIdeSelection.js';
const MAX_IDE_HINT_SHOW_COUNT = 5
const MAX_IDE_HINT_SHOW_COUNT = 5;
type Props = {
ideInstallationStatus: IDEExtensionInstallationStatus | null
ideSelection: IDESelection | undefined
mcpClients: MCPServerConnection[]
}
ideInstallationStatus: IDEExtensionInstallationStatus | null;
ideSelection: IDESelection | undefined;
mcpClients: MCPServerConnection[];
};
export function useIDEStatusIndicator({
ideSelection,
mcpClients,
ideInstallationStatus,
}: Props): void {
const { addNotification, removeNotification } = useNotifications()
const { status: ideStatus, ideName } = useIdeConnectionStatus(mcpClients)
const hasShownHintRef = useRef(false)
export function useIDEStatusIndicator({ ideSelection, mcpClients, ideInstallationStatus }: Props): void {
const { addNotification, removeNotification } = useNotifications();
const { status: ideStatus, ideName } = useIdeConnectionStatus(mcpClients);
const hasShownHintRef = useRef(false);
const isJetBrains = ideInstallationStatus
? isJetBrainsIde(ideInstallationStatus?.ideType)
: false
const showIDEInstallErrorOrJetBrainsInfo =
ideInstallationStatus?.error || isJetBrains
const isJetBrains = ideInstallationStatus ? isJetBrainsIde(ideInstallationStatus?.ideType) : false;
const showIDEInstallErrorOrJetBrainsInfo = ideInstallationStatus?.error || isJetBrains;
const shouldShowIdeSelection =
ideStatus === 'connected' &&
(ideSelection?.filePath ||
(ideSelection?.text && ideSelection.lineCount > 0))
ideStatus === 'connected' && (ideSelection?.filePath || (ideSelection?.text && ideSelection.lineCount > 0));
// Only show the connected if not showing context
const shouldShowConnected =
ideStatus === 'connected' && !shouldShowIdeSelection
const shouldShowConnected = ideStatus === 'connected' && !shouldShowIdeSelection;
const showIDEInstallError =
showIDEInstallErrorOrJetBrainsInfo &&
!isJetBrains &&
!shouldShowConnected &&
!shouldShowIdeSelection
showIDEInstallErrorOrJetBrainsInfo && !isJetBrains && !shouldShowConnected && !shouldShowIdeSelection;
const showJetBrainsInfo =
showIDEInstallErrorOrJetBrainsInfo &&
isJetBrains &&
!shouldShowConnected &&
!shouldShowIdeSelection
showIDEInstallErrorOrJetBrainsInfo && isJetBrains && !shouldShowConnected && !shouldShowIdeSelection;
// Show the /ide command hint if running from an external terminal and found running IDE(s)
// Delay showing hint to avoid brief flash during auto-connect startup
useEffect(() => {
if (getIsRemoteMode()) return
if (getIsRemoteMode()) return;
if (isSupportedTerminal() || ideStatus !== null || showJetBrainsInfo) {
removeNotification('ide-status-hint')
return
removeNotification('ide-status-hint');
return;
}
// Wait a bit to let auto-connect happen first, avoiding brief hint flash
if (
hasShownHintRef.current ||
(getGlobalConfig().ideHintShownCount ?? 0) >= MAX_IDE_HINT_SHOW_COUNT
) {
return
if (hasShownHintRef.current || (getGlobalConfig().ideHintShownCount ?? 0) >= MAX_IDE_HINT_SHOW_COUNT) {
return;
}
const timeoutId = setTimeout(
(hasShownHintRef, addNotification) => {
void detectIDEs(true).then(infos => {
const ideName = infos[0]?.name
const ideName = infos[0]?.name;
if (ideName && !hasShownHintRef.current) {
hasShownHintRef.current = true
hasShownHintRef.current = true;
saveGlobalConfig(current => ({
...current,
ideHintShownCount: (current.ideHintShownCount ?? 0) + 1,
}))
}));
addNotification({
key: 'ide-status-hint',
jsx: (
@@ -90,70 +66,58 @@ export function useIDEStatusIndicator({
</Text>
),
priority: 'low',
})
});
}
})
});
},
3000,
hasShownHintRef,
addNotification,
)
return () => clearTimeout(timeoutId)
}, [addNotification, removeNotification, ideStatus, showJetBrainsInfo])
);
return () => clearTimeout(timeoutId);
}, [addNotification, removeNotification, ideStatus, showJetBrainsInfo]);
// Show IDE disconnected/failed notification when status is disconnected
useEffect(() => {
if (getIsRemoteMode()) return
if (
showIDEInstallError ||
showJetBrainsInfo ||
ideStatus !== 'disconnected' ||
!ideName
) {
removeNotification('ide-status-disconnected')
return
if (getIsRemoteMode()) return;
if (showIDEInstallError || showJetBrainsInfo || ideStatus !== 'disconnected' || !ideName) {
removeNotification('ide-status-disconnected');
return;
}
addNotification({
key: 'ide-status-disconnected',
text: `${ideName} disconnected`,
color: 'error',
priority: 'medium',
})
}, [
addNotification,
removeNotification,
ideStatus,
ideName,
showIDEInstallError,
showJetBrainsInfo,
])
});
}, [addNotification, removeNotification, ideStatus, ideName, showIDEInstallError, showJetBrainsInfo]);
// Show JetBrains plugin not connected hint
useEffect(() => {
if (getIsRemoteMode()) return
if (getIsRemoteMode()) return;
if (!showJetBrainsInfo) {
removeNotification('ide-status-jetbrains-disconnected')
return
removeNotification('ide-status-jetbrains-disconnected');
return;
}
addNotification({
key: 'ide-status-jetbrains-disconnected',
text: 'IDE plugin not connected · /status for info',
priority: 'medium',
})
}, [addNotification, removeNotification, showJetBrainsInfo])
});
}, [addNotification, removeNotification, showJetBrainsInfo]);
// Show IDE install error
useEffect(() => {
if (getIsRemoteMode()) return
if (getIsRemoteMode()) return;
if (!showIDEInstallError) {
removeNotification('ide-status-install-error')
return
removeNotification('ide-status-install-error');
return;
}
addNotification({
key: 'ide-status-install-error',
text: 'IDE extension install failed (see /status for info)',
color: 'error',
priority: 'medium',
})
}, [addNotification, removeNotification, showIDEInstallError])
});
}, [addNotification, removeNotification, showIDEInstallError]);
}

View File

@@ -1,22 +1,22 @@
import { checkInstall } from 'src/utils/nativeInstaller/index.js'
import { useStartupNotification } from './useStartupNotification.js'
import { checkInstall } from 'src/utils/nativeInstaller/index.js';
import { useStartupNotification } from './useStartupNotification.js';
export function useInstallMessages(): void {
useStartupNotification(async () => {
const messages = await checkInstall()
const messages = await checkInstall();
return messages.map((message, index) => {
let priority: 'low' | 'medium' | 'high' | 'immediate' = 'low'
let priority: 'low' | 'medium' | 'high' | 'immediate' = 'low';
if (message.type === 'error' || message.userActionRequired) {
priority = 'high'
priority = 'high';
} else if (message.type === 'path' || message.type === 'alias') {
priority = 'medium'
priority = 'medium';
}
return {
key: `install-message-${index}-${message.type}`,
text: message.message,
priority,
color: message.type === 'error' ? 'error' : 'warning',
}
})
})
};
});
});
}

View File

@@ -1,17 +1,14 @@
import * as React from 'react'
import { useInterval } from 'usehooks-ts'
import { getIsRemoteMode, getIsScrollDraining } from '../../bootstrap/state.js'
import { useNotifications } from '../../context/notifications.js'
import { Text } from '@anthropic/ink'
import {
getInitializationStatus,
getLspServerManager,
} from '../../services/lsp/manager.js'
import { useSetAppState } from '../../state/AppState.js'
import { logForDebugging } from '../../utils/debug.js'
import { isEnvTruthy } from '../../utils/envUtils.js'
import * as React from 'react';
import { useInterval } from 'usehooks-ts';
import { getIsRemoteMode, getIsScrollDraining } from '../../bootstrap/state.js';
import { useNotifications } from '../../context/notifications.js';
import { Text } from '@anthropic/ink';
import { getInitializationStatus, getLspServerManager } from '../../services/lsp/manager.js';
import { useSetAppState } from '../../state/AppState.js';
import { logForDebugging } from '../../utils/debug.js';
import { isEnvTruthy } from '../../utils/envUtils.js';
const LSP_POLL_INTERVAL_MS = 5000
const LSP_POLL_INTERVAL_MS = 5000;
/**
* Hook that polls LSP status and shows a notification when:
@@ -23,27 +20,25 @@ const LSP_POLL_INTERVAL_MS = 5000
* Only active when ENABLE_LSP_TOOL is set.
*/
export function useLspInitializationNotification(): void {
const { addNotification } = useNotifications()
const setAppState = useSetAppState()
const { addNotification } = useNotifications();
const setAppState = useSetAppState();
// Lazy initializer — eager form re-evaluates isEnvTruthy on every REPL
// render (the arg expression runs even though useState ignores it after
// mount). Showed up as 7.2s isEnvTruthy self-time during PageUp spam
// after #24498 swapped cheap !!process.env.X for isEnvTruthy().
const [shouldPoll, setShouldPoll] = React.useState(() =>
isEnvTruthy("true"),
)
const [shouldPoll, setShouldPoll] = React.useState(() => isEnvTruthy('true'));
// Track which errors we've already notified about to avoid duplicates
const notifiedErrorsRef = React.useRef<Set<string>>(new Set())
const notifiedErrorsRef = React.useRef<Set<string>>(new Set());
const addError = React.useCallback(
(source: string, errorMessage: string) => {
const errorKey = `${source}:${errorMessage}`
const errorKey = `${source}:${errorMessage}`;
if (notifiedErrorsRef.current.has(errorKey)) {
return // Already notified
return; // Already notified
}
notifiedErrorsRef.current.add(errorKey)
notifiedErrorsRef.current.add(errorKey);
logForDebugging(`LSP error: ${source} - ${errorMessage}`)
logForDebugging(`LSP error: ${source} - ${errorMessage}`);
// Add error to appState.plugins.errors
setAppState(prev => {
@@ -51,15 +46,15 @@ export function useLspInitializationNotification(): void {
const existingKeys = new Set(
prev.plugins.errors.map(e => {
if (e.type === 'generic-error') {
return `generic-error:${e.source}:${e.error}`
return `generic-error:${e.source}:${e.error}`;
}
return `${e.type}:${e.source}`
return `${e.type}:${e.source}`;
}),
)
);
const stateErrorKey = `generic-error:${source}:${errorMessage}`
const stateErrorKey = `generic-error:${source}:${errorMessage}`;
if (existingKeys.has(stateErrorKey)) {
return prev
return prev;
}
return {
@@ -75,13 +70,11 @@ export function useLspInitializationNotification(): void {
},
],
},
}
})
};
});
// Show notification - extract plugin name from source like "plugin:typescript-lsp:typescript"
const displayName = source.startsWith('plugin:')
? (source.split(':')[1] ?? source)
: source
const displayName = source.startsWith('plugin:') ? (source.split(':')[1] ?? source) : source;
addNotification({
key: `lsp-error-${source}`,
@@ -93,49 +86,49 @@ export function useLspInitializationNotification(): void {
),
priority: 'medium',
timeoutMs: 8000,
})
});
},
[addNotification, setAppState],
)
);
const poll = React.useCallback(() => {
if (getIsRemoteMode()) return
if (getIsRemoteMode()) return;
// Skip during scroll drain — iterating all LSP servers + setAppState
// competes for the event loop with scroll frames. Next interval picks up.
if (getIsScrollDraining()) return
if (getIsScrollDraining()) return;
const status = getInitializationStatus()
const status = getInitializationStatus();
// Check manager initialization status
if (status.status === 'failed') {
addError('lsp-manager', status.error.message)
setShouldPoll(false)
return
addError('lsp-manager', status.error.message);
setShouldPoll(false);
return;
}
if (status.status === 'pending' || status.status === 'not-started') {
// Still initializing, continue polling
return
return;
}
// Manager initialized successfully - check for server errors
const manager = getLspServerManager()
const manager = getLspServerManager();
if (manager) {
const servers = manager.getAllServers()
const servers = manager.getAllServers();
for (const [serverName, server] of servers) {
if (server.state === 'error' && server.lastError) {
addError(serverName, server.lastError.message)
addError(serverName, server.lastError.message);
}
}
}
// Continue polling to detect future server errors
}, [addError])
}, [addError]);
useInterval(poll, shouldPoll ? LSP_POLL_INTERVAL_MS : null)
useInterval(poll, shouldPoll ? LSP_POLL_INTERVAL_MS : null);
// Initial poll on mount
React.useEffect(() => {
if (getIsRemoteMode() || !shouldPoll) return
poll()
}, [poll, shouldPoll])
if (getIsRemoteMode() || !shouldPoll) return;
poll();
}, [poll, shouldPoll]);
}

View File

@@ -1,30 +1,28 @@
import * as React from 'react'
import { useEffect } from 'react'
import { useNotifications } from 'src/context/notifications.js'
import { getIsRemoteMode } from '../../bootstrap/state.js'
import { Text } from '@anthropic/ink'
import { hasClaudeAiMcpEverConnected } from '../../services/mcp/claudeai.js'
import type { MCPServerConnection } from '../../services/mcp/types.js'
import * as React from 'react';
import { useEffect } from 'react';
import { useNotifications } from 'src/context/notifications.js';
import { getIsRemoteMode } from '../../bootstrap/state.js';
import { Text } from '@anthropic/ink';
import { hasClaudeAiMcpEverConnected } from '../../services/mcp/claudeai.js';
import type { MCPServerConnection } from '../../services/mcp/types.js';
type Props = {
mcpClients?: MCPServerConnection[]
}
mcpClients?: MCPServerConnection[];
};
const EMPTY_MCP_CLIENTS: MCPServerConnection[] = []
const EMPTY_MCP_CLIENTS: MCPServerConnection[] = [];
export function useMcpConnectivityStatus({
mcpClients = EMPTY_MCP_CLIENTS,
}: Props): void {
const { addNotification } = useNotifications()
export function useMcpConnectivityStatus({ mcpClients = EMPTY_MCP_CLIENTS }: Props): void {
const { addNotification } = useNotifications();
useEffect(() => {
if (getIsRemoteMode()) return
if (getIsRemoteMode()) return;
const failedLocalClients = mcpClients.filter(
client =>
client.type === 'failed' &&
client.config.type !== 'sse-ide' &&
client.config.type !== 'ws-ide' &&
client.config.type !== 'claudeai-proxy',
)
);
// claude.ai failures get a separate notification: they almost always indicate
// a toolbox-service outage (shared auth backend), not a local config issue.
// Only flag connectors that have previously connected successfully — an
@@ -33,27 +31,24 @@ export function useMcpConnectivityStatus({
// yesterday and is now failed is a state change worth surfacing.
const failedClaudeAiClients = mcpClients.filter(
client =>
client.type === 'failed' &&
client.config.type === 'claudeai-proxy' &&
hasClaudeAiMcpEverConnected(client.name),
)
client.type === 'failed' && client.config.type === 'claudeai-proxy' && hasClaudeAiMcpEverConnected(client.name),
);
const needsAuthLocalServers = mcpClients.filter(
client =>
client.type === 'needs-auth' && client.config.type !== 'claudeai-proxy',
)
client => client.type === 'needs-auth' && client.config.type !== 'claudeai-proxy',
);
const needsAuthClaudeAiServers = mcpClients.filter(
client =>
client.type === 'needs-auth' &&
client.config.type === 'claudeai-proxy' &&
hasClaudeAiMcpEverConnected(client.name),
)
);
if (
failedLocalClients.length === 0 &&
failedClaudeAiClients.length === 0 &&
needsAuthLocalServers.length === 0 &&
needsAuthClaudeAiServers.length === 0
) {
return
return;
}
if (failedLocalClients.length > 0) {
addNotification({
@@ -61,14 +56,13 @@ export function useMcpConnectivityStatus({
jsx: (
<>
<Text color="error">
{failedLocalClients.length} MCP{' '}
{failedLocalClients.length === 1 ? 'server' : 'servers'} failed
{failedLocalClients.length} MCP {failedLocalClients.length === 1 ? 'server' : 'servers'} failed
</Text>
<Text dimColor> · /mcp</Text>
</>
),
priority: 'medium',
})
});
}
if (failedClaudeAiClients.length > 0) {
addNotification({
@@ -76,15 +70,14 @@ export function useMcpConnectivityStatus({
jsx: (
<>
<Text color="error">
{failedClaudeAiClients.length} claude.ai{' '}
{failedClaudeAiClients.length === 1 ? 'connector' : 'connectors'}{' '}
{failedClaudeAiClients.length} claude.ai {failedClaudeAiClients.length === 1 ? 'connector' : 'connectors'}{' '}
unavailable
</Text>
<Text dimColor> · /mcp</Text>
</>
),
priority: 'medium',
})
});
}
if (needsAuthLocalServers.length > 0) {
addNotification({
@@ -92,17 +85,14 @@ export function useMcpConnectivityStatus({
jsx: (
<>
<Text color="warning">
{needsAuthLocalServers.length} MCP{' '}
{needsAuthLocalServers.length === 1
? 'server needs'
: 'servers need'}{' '}
{needsAuthLocalServers.length} MCP {needsAuthLocalServers.length === 1 ? 'server needs' : 'servers need'}{' '}
auth
</Text>
<Text dimColor> · /mcp</Text>
</>
),
priority: 'medium',
})
});
}
if (needsAuthClaudeAiServers.length > 0) {
addNotification({
@@ -111,16 +101,13 @@ export function useMcpConnectivityStatus({
<>
<Text color="warning">
{needsAuthClaudeAiServers.length} claude.ai{' '}
{needsAuthClaudeAiServers.length === 1
? 'connector needs'
: 'connectors need'}{' '}
auth
{needsAuthClaudeAiServers.length === 1 ? 'connector needs' : 'connectors need'} auth
</Text>
<Text dimColor> · /mcp</Text>
</>
),
priority: 'medium',
})
});
}
}, [addNotification, mcpClients])
}, [addNotification, mcpClients]);
}

View File

@@ -1,6 +1,6 @@
import type { Notification } from 'src/context/notifications.js'
import { type GlobalConfig, getGlobalConfig } from 'src/utils/config.js'
import { useStartupNotification } from './useStartupNotification.js'
import type { Notification } from 'src/context/notifications.js';
import { type GlobalConfig, getGlobalConfig } from 'src/utils/config.js';
import { useStartupNotification } from './useStartupNotification.js';
// Shows a one-time notification right after a model migration writes its
// timestamp to config. Each entry reads its own timestamp field(s) and emits
@@ -9,21 +9,21 @@ import { useStartupNotification } from './useStartupNotification.js'
const MIGRATIONS: ((c: GlobalConfig) => Notification | undefined)[] = [
// Sonnet 4.5 → 4.6 (pro/max/team premium)
c => {
if (!recent(c.sonnet45To46MigrationTimestamp)) return
if (!recent(c.sonnet45To46MigrationTimestamp)) return;
return {
key: 'sonnet-46-update',
text: 'Model updated to Sonnet 4.6',
color: 'suggestion',
priority: 'high',
timeoutMs: 3000,
}
};
},
// Opus Pro → default, or pinned 4.0/4.1 → opus alias. Both land on the
// current Opus default (4.7 for 1P).
c => {
const isLegacyRemap = Boolean(c.legacyOpusMigrationTimestamp)
const ts = c.legacyOpusMigrationTimestamp ?? c.opusProMigrationTimestamp
if (!recent(ts)) return
const isLegacyRemap = Boolean(c.legacyOpusMigrationTimestamp);
const ts = c.legacyOpusMigrationTimestamp ?? c.opusProMigrationTimestamp;
if (!recent(ts)) return;
return {
key: 'opus-pro-update',
text: isLegacyRemap
@@ -32,22 +32,22 @@ const MIGRATIONS: ((c: GlobalConfig) => Notification | undefined)[] = [
color: 'suggestion',
priority: 'high',
timeoutMs: isLegacyRemap ? 8000 : 3000,
}
};
},
]
];
export function useModelMigrationNotifications(): void {
useStartupNotification(() => {
const config = getGlobalConfig()
const notifs: Notification[] = []
const config = getGlobalConfig();
const notifs: Notification[] = [];
for (const migration of MIGRATIONS) {
const notif = migration(config)
if (notif) notifs.push(notif)
const notif = migration(config);
if (notif) notifs.push(notif);
}
return notifs.length > 0 ? notifs : null
})
return notifs.length > 0 ? notifs : null;
});
}
function recent(ts: number | undefined): boolean {
return ts !== undefined && Date.now() - ts < 3000
return ts !== undefined && Date.now() - ts < 3000;
}

View File

@@ -1,27 +1,23 @@
import { isInBundledMode } from 'src/utils/bundledMode.js'
import { getCurrentInstallationType } from 'src/utils/doctorDiagnostic.js'
import { isEnvTruthy } from 'src/utils/envUtils.js'
import { useStartupNotification } from './useStartupNotification.js'
import { isInBundledMode } from 'src/utils/bundledMode.js';
import { getCurrentInstallationType } from 'src/utils/doctorDiagnostic.js';
import { isEnvTruthy } from 'src/utils/envUtils.js';
import { useStartupNotification } from './useStartupNotification.js';
const NPM_DEPRECATION_MESSAGE =
''
const NPM_DEPRECATION_MESSAGE = '';
export function useNpmDeprecationNotification(): void {
useStartupNotification(async () => {
if (
isInBundledMode() ||
isEnvTruthy(process.env.DISABLE_INSTALLATION_CHECKS)
) {
return null
if (isInBundledMode() || isEnvTruthy(process.env.DISABLE_INSTALLATION_CHECKS)) {
return null;
}
const installationType = await getCurrentInstallationType()
if (installationType === 'development') return null
const installationType = await getCurrentInstallationType();
if (installationType === 'development') return null;
return {
timeoutMs: 15000,
key: 'npm-deprecation-warning',
text: NPM_DEPRECATION_MESSAGE,
color: 'warning',
priority: 'high',
}
})
};
});
}

View File

@@ -1,67 +1,59 @@
import * as React from 'react'
import { useEffect, useState } from 'react'
import { getIsRemoteMode } from '../../bootstrap/state.js'
import { useNotifications } from '../../context/notifications.js'
import { Text } from '@anthropic/ink'
import { logForDebugging } from '../../utils/debug.js'
import { onPluginsAutoUpdated } from '../../utils/plugins/pluginAutoupdate.js'
import * as React from 'react';
import { useEffect, useState } from 'react';
import { getIsRemoteMode } from '../../bootstrap/state.js';
import { useNotifications } from '../../context/notifications.js';
import { Text } from '@anthropic/ink';
import { logForDebugging } from '../../utils/debug.js';
import { onPluginsAutoUpdated } from '../../utils/plugins/pluginAutoupdate.js';
/**
* Hook that displays a notification when plugins have been auto-updated.
* The notification tells the user to run /reload-plugins to apply the updates.
*/
export function usePluginAutoupdateNotification(): void {
const { addNotification } = useNotifications()
const [updatedPlugins, setUpdatedPlugins] = useState<string[]>([])
const { addNotification } = useNotifications();
const [updatedPlugins, setUpdatedPlugins] = useState<string[]>([]);
// Register for autoupdate notifications
useEffect(() => {
if (getIsRemoteMode()) return
if (getIsRemoteMode()) return;
const unsubscribe = onPluginsAutoUpdated(plugins => {
logForDebugging(
`Plugin autoupdate notification: ${plugins.length} plugin(s) updated`,
)
setUpdatedPlugins(plugins)
})
logForDebugging(`Plugin autoupdate notification: ${plugins.length} plugin(s) updated`);
setUpdatedPlugins(plugins);
});
return unsubscribe
}, [])
return unsubscribe;
}, []);
// Show notification when plugins are updated
useEffect(() => {
if (getIsRemoteMode()) return
if (getIsRemoteMode()) return;
if (updatedPlugins.length === 0) {
return
return;
}
// Extract plugin names from plugin IDs (format: "name@marketplace")
const pluginNames = updatedPlugins.map(id => {
const atIndex = id.indexOf('@')
return atIndex > 0 ? id.substring(0, atIndex) : id
})
const atIndex = id.indexOf('@');
return atIndex > 0 ? id.substring(0, atIndex) : id;
});
const displayNames =
pluginNames.length <= 2
? pluginNames.join(' and ')
: `${pluginNames.length} plugins`
const displayNames = pluginNames.length <= 2 ? pluginNames.join(' and ') : `${pluginNames.length} plugins`;
addNotification({
key: 'plugin-autoupdate-restart',
jsx: (
<>
<Text color="success">
{pluginNames.length === 1 ? 'Plugin' : 'Plugins'} updated:{' '}
{displayNames}
{pluginNames.length === 1 ? 'Plugin' : 'Plugins'} updated: {displayNames}
</Text>
<Text dimColor> · Run /reload-plugins to apply</Text>
</>
),
priority: 'low',
timeoutMs: 10000,
})
});
logForDebugging(
`Showing plugin autoupdate notification for: ${pluginNames.join(', ')}`,
)
}, [updatedPlugins, addNotification])
logForDebugging(`Showing plugin autoupdate notification for: ${pluginNames.join(', ')}`);
}, [updatedPlugins, addNotification]);
}

View File

@@ -1,64 +1,57 @@
import * as React from 'react'
import { useEffect, useMemo } from 'react'
import { getIsRemoteMode } from '../../bootstrap/state.js'
import { useNotifications } from '../../context/notifications.js'
import { Text } from '@anthropic/ink'
import { useAppState } from '../../state/AppState.js'
import { logForDebugging } from '../../utils/debug.js'
import { plural } from '../../utils/stringUtils.js'
import * as React from 'react';
import { useEffect, useMemo } from 'react';
import { getIsRemoteMode } from '../../bootstrap/state.js';
import { useNotifications } from '../../context/notifications.js';
import { Text } from '@anthropic/ink';
import { useAppState } from '../../state/AppState.js';
import { logForDebugging } from '../../utils/debug.js';
import { plural } from '../../utils/stringUtils.js';
export function usePluginInstallationStatus(): void {
const { addNotification } = useNotifications()
const installationStatus = useAppState(s => s.plugins.installationStatus)
const { addNotification } = useNotifications();
const installationStatus = useAppState(s => s.plugins.installationStatus);
// Memoize the failed counts to prevent unnecessary effect triggers
const { totalFailed, failedMarketplacesCount, failedPluginsCount } =
useMemo(() => {
if (!installationStatus) {
return {
totalFailed: 0,
failedMarketplacesCount: 0,
failedPluginsCount: 0,
}
}
const failedMarketplaces = installationStatus.marketplaces.filter(
m => m.status === 'failed',
)
const failedPlugins = installationStatus.plugins.filter(
p => p.status === 'failed',
)
const { totalFailed, failedMarketplacesCount, failedPluginsCount } = useMemo(() => {
if (!installationStatus) {
return {
totalFailed: failedMarketplaces.length + failedPlugins.length,
failedMarketplacesCount: failedMarketplaces.length,
failedPluginsCount: failedPlugins.length,
}
}, [installationStatus])
totalFailed: 0,
failedMarketplacesCount: 0,
failedPluginsCount: 0,
};
}
const failedMarketplaces = installationStatus.marketplaces.filter(m => m.status === 'failed');
const failedPlugins = installationStatus.plugins.filter(p => p.status === 'failed');
return {
totalFailed: failedMarketplaces.length + failedPlugins.length,
failedMarketplacesCount: failedMarketplaces.length,
failedPluginsCount: failedPlugins.length,
};
}, [installationStatus]);
useEffect(() => {
if (getIsRemoteMode()) return
if (getIsRemoteMode()) return;
if (!installationStatus) {
logForDebugging('No installation status to monitor')
return
logForDebugging('No installation status to monitor');
return;
}
if (totalFailed === 0) {
return
return;
}
logForDebugging(
`Plugin installation status: ${failedMarketplacesCount} failed marketplaces, ${failedPluginsCount} failed plugins`,
)
);
if (totalFailed === 0) {
return
return;
}
// Add notification for failures
logForDebugging(
`Adding notification for ${totalFailed} failed installations`,
)
logForDebugging(`Adding notification for ${totalFailed} failed installations`);
addNotification({
key: 'plugin-install-failed',
jsx: (
@@ -70,11 +63,6 @@ export function usePluginInstallationStatus(): void {
</>
),
priority: 'medium',
})
}, [
addNotification,
totalFailed,
failedMarketplacesCount,
failedPluginsCount,
])
});
}, [addNotification, totalFailed, failedMarketplacesCount, failedPluginsCount]);
}

View File

@@ -1,56 +1,41 @@
import * as React from 'react'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useNotifications } from 'src/context/notifications.js'
import { Text } from '@anthropic/ink'
import {
getRateLimitWarning,
getUsingOverageText,
} from 'src/services/claudeAiLimits.js'
import { useClaudeAiLimits } from 'src/services/claudeAiLimitsHook.js'
import { getSubscriptionType } from 'src/utils/auth.js'
import { hasClaudeAiBillingAccess } from 'src/utils/billing.js'
import { getIsRemoteMode } from '../../bootstrap/state.js'
import * as React from 'react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useNotifications } from 'src/context/notifications.js';
import { Text } from '@anthropic/ink';
import { getRateLimitWarning, getUsingOverageText } from 'src/services/claudeAiLimits.js';
import { useClaudeAiLimits } from 'src/services/claudeAiLimitsHook.js';
import { getSubscriptionType } from 'src/utils/auth.js';
import { hasClaudeAiBillingAccess } from 'src/utils/billing.js';
import { getIsRemoteMode } from '../../bootstrap/state.js';
export function useRateLimitWarningNotification(model: string): void {
const { addNotification } = useNotifications()
const claudeAiLimits = useClaudeAiLimits()
const { addNotification } = useNotifications();
const claudeAiLimits = useClaudeAiLimits();
// claudeAiLimits reference is stable until statusListeners fire (API
// response), so these skip the Intl formatting work on most REPL renders.
const rateLimitWarning = useMemo(
() => getRateLimitWarning(claudeAiLimits, model),
[claudeAiLimits, model],
)
const usingOverageText = useMemo(
() => getUsingOverageText(claudeAiLimits),
[claudeAiLimits],
)
const shownWarningRef = useRef<string | null>(null)
const subscriptionType = getSubscriptionType()
const hasBillingAccess = hasClaudeAiBillingAccess()
const isTeamOrEnterprise =
subscriptionType === 'team' || subscriptionType === 'enterprise'
const rateLimitWarning = useMemo(() => getRateLimitWarning(claudeAiLimits, model), [claudeAiLimits, model]);
const usingOverageText = useMemo(() => getUsingOverageText(claudeAiLimits), [claudeAiLimits]);
const shownWarningRef = useRef<string | null>(null);
const subscriptionType = getSubscriptionType();
const hasBillingAccess = hasClaudeAiBillingAccess();
const isTeamOrEnterprise = subscriptionType === 'team' || subscriptionType === 'enterprise';
// Track overage mode transitions
const [hasShownOverageNotification, setHasShownOverageNotification] =
useState(false)
const [hasShownOverageNotification, setHasShownOverageNotification] = useState(false);
// Show immediate notification when entering overage mode
useEffect(() => {
if (getIsRemoteMode()) return
if (
claudeAiLimits.isUsingOverage &&
!hasShownOverageNotification &&
(!isTeamOrEnterprise || hasBillingAccess)
) {
if (getIsRemoteMode()) return;
if (claudeAiLimits.isUsingOverage && !hasShownOverageNotification && (!isTeamOrEnterprise || hasBillingAccess)) {
addNotification({
key: 'limit-reached',
text: usingOverageText,
priority: 'immediate',
})
setHasShownOverageNotification(true)
});
setHasShownOverageNotification(true);
} else if (!claudeAiLimits.isUsingOverage && hasShownOverageNotification) {
// Reset when no longer in overage mode
setHasShownOverageNotification(false)
setHasShownOverageNotification(false);
}
}, [
claudeAiLimits.isUsingOverage,
@@ -59,13 +44,13 @@ export function useRateLimitWarningNotification(model: string): void {
addNotification,
hasBillingAccess,
isTeamOrEnterprise,
])
]);
// Show warning notification for approaching limits
useEffect(() => {
if (getIsRemoteMode()) return
if (getIsRemoteMode()) return;
if (rateLimitWarning && rateLimitWarning !== shownWarningRef.current) {
shownWarningRef.current = rateLimitWarning
shownWarningRef.current = rateLimitWarning;
addNotification({
key: 'rate-limit-warning',
jsx: (
@@ -74,7 +59,7 @@ export function useRateLimitWarningNotification(model: string): void {
</Text>
),
priority: 'high',
})
});
}
}, [rateLimitWarning, addNotification])
}, [rateLimitWarning, addNotification]);
}

View File

@@ -1,41 +1,41 @@
import { useCallback, useEffect, useState } from 'react'
import { useNotifications } from 'src/context/notifications.js'
import { getIsRemoteMode } from '../../bootstrap/state.js'
import { getSettingsWithAllErrors } from '../../utils/settings/allErrors.js'
import type { ValidationError } from '../../utils/settings/validation.js'
import { useSettingsChange } from '../useSettingsChange.js'
import { useCallback, useEffect, useState } from 'react';
import { useNotifications } from 'src/context/notifications.js';
import { getIsRemoteMode } from '../../bootstrap/state.js';
import { getSettingsWithAllErrors } from '../../utils/settings/allErrors.js';
import type { ValidationError } from '../../utils/settings/validation.js';
import { useSettingsChange } from '../useSettingsChange.js';
const SETTINGS_ERRORS_NOTIFICATION_KEY = 'settings-errors'
const SETTINGS_ERRORS_NOTIFICATION_KEY = 'settings-errors';
export function useSettingsErrors(): ValidationError[] {
const { addNotification, removeNotification } = useNotifications()
const { addNotification, removeNotification } = useNotifications();
const [errors, setErrors] = useState<ValidationError[]>(() => {
const { errors } = getSettingsWithAllErrors()
return errors
})
const { errors } = getSettingsWithAllErrors();
return errors;
});
const handleSettingsChange = useCallback(() => {
const { errors } = getSettingsWithAllErrors()
setErrors(errors)
}, [])
const { errors } = getSettingsWithAllErrors();
setErrors(errors);
}, []);
useSettingsChange(handleSettingsChange)
useSettingsChange(handleSettingsChange);
useEffect(() => {
if (getIsRemoteMode()) return
if (getIsRemoteMode()) return;
if (errors.length > 0) {
const message = `Found ${errors.length} settings ${errors.length === 1 ? 'issue' : 'issues'} · /doctor for details`
const message = `Found ${errors.length} settings ${errors.length === 1 ? 'issue' : 'issues'} · /doctor for details`;
addNotification({
key: SETTINGS_ERRORS_NOTIFICATION_KEY,
text: message,
color: 'warning',
priority: 'high',
timeoutMs: 60000,
})
});
} else {
removeNotification(SETTINGS_ERRORS_NOTIFICATION_KEY)
removeNotification(SETTINGS_ERRORS_NOTIFICATION_KEY);
}
}, [errors, addNotification, removeNotification])
}, [errors, addNotification, removeNotification]);
return errors
return errors;
}