mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 08:15:53 +00:00
style: 完成所有文件的lint
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
})
|
||||
})
|
||||
};
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user