({
server,
serverToolsCount,
onViewTools,
onCancel,
onComplete,
borderless = false
}: Props)
| 39 | borderless?: boolean; |
| 40 | }; |
| 41 | export function MCPRemoteServerMenu({ |
| 42 | server, |
| 43 | serverToolsCount, |
| 44 | onViewTools, |
| 45 | onCancel, |
| 46 | onComplete, |
| 47 | borderless = false |
| 48 | }: Props): React.ReactNode { |
| 49 | const [theme] = useTheme(); |
| 50 | const exitState = useExitOnCtrlCDWithKeybindings(); |
| 51 | const { |
| 52 | columns: terminalColumns |
| 53 | } = useTerminalSize(); |
| 54 | const [isAuthenticating, setIsAuthenticating] = React.useState(false); |
| 55 | const [error, setError] = React.useState<string | null>(null); |
| 56 | const mcp = useAppState(s => s.mcp); |
| 57 | const setAppState = useSetAppState(); |
| 58 | const [authorizationUrl, setAuthorizationUrl] = React.useState<string | null>(null); |
| 59 | const [isReconnecting, setIsReconnecting] = useState(false); |
| 60 | const authAbortControllerRef = useRef<AbortController | null>(null); |
| 61 | const [isClaudeAIAuthenticating, setIsClaudeAIAuthenticating] = useState(false); |
| 62 | const [claudeAIAuthUrl, setClaudeAIAuthUrl] = useState<string | null>(null); |
| 63 | const [isClaudeAIClearingAuth, setIsClaudeAIClearingAuth] = useState(false); |
| 64 | const [claudeAIClearAuthUrl, setClaudeAIClearAuthUrl] = useState<string | null>(null); |
| 65 | const [claudeAIClearAuthBrowserOpened, setClaudeAIClearAuthBrowserOpened] = useState(false); |
| 66 | const [urlCopied, setUrlCopied] = useState(false); |
| 67 | const copyTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined); |
| 68 | const unmountedRef = useRef(false); |
| 69 | const [callbackUrlInput, setCallbackUrlInput] = useState(''); |
| 70 | const [callbackUrlCursorOffset, setCallbackUrlCursorOffset] = useState(0); |
| 71 | const [manualCallbackSubmit, setManualCallbackSubmit] = useState<((url: string) => void) | null>(null); |
| 72 | |
| 73 | // If the component unmounts mid-auth (e.g. a parent component's Esc handler |
| 74 | // navigates away before ours fires), abort the OAuth flow so the callback |
| 75 | // server is closed. Without this, the server stays bound and the process |
| 76 | // can outlive the terminal. Also clear the copy-feedback timer and mark |
| 77 | // unmounted so the async setClipboard callback doesn't setUrlCopied / |
| 78 | // schedule a new timer after unmount. |
| 79 | useEffect(() => () => { |
| 80 | unmountedRef.current = true; |
| 81 | authAbortControllerRef.current?.abort(); |
| 82 | if (copyTimeoutRef.current !== undefined) { |
| 83 | clearTimeout(copyTimeoutRef.current); |
| 84 | } |
| 85 | }, []); |
| 86 | |
| 87 | // A server is effectively authenticated if: |
| 88 | // 1. It has OAuth tokens (server.isAuthenticated), OR |
| 89 | // 2. It's connected and has tools (meaning it's working via some auth mechanism) |
| 90 | const isEffectivelyAuthenticated = server.isAuthenticated || server.client.type === 'connected' && serverToolsCount > 0; |
| 91 | const reconnectMcpServer = useMcpReconnect(); |
| 92 | const handleClaudeAIAuthComplete = React.useCallback(async () => { |
| 93 | setIsClaudeAIAuthenticating(false); |
| 94 | setClaudeAIAuthUrl(null); |
| 95 | setIsReconnecting(true); |
| 96 | try { |
| 97 | const result = await reconnectMcpServer(server.name); |
| 98 | const success = result.client.type === 'connected'; |
nothing calls this directly
no test coverage detected