Files
claude-code/src/components/mcp/MCPRemoteServerMenu.tsx
2026-04-06 23:56:45 +08:00

895 lines
28 KiB
TypeScript

import figures from 'figures'
import React, { useEffect, useRef, useState } from 'react'
import {
type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
logEvent,
} from 'src/services/analytics/index.js'
import type { CommandResultDisplay } from '../../commands.js'
import { getOauthConfig } from '../../constants/oauth.js'
import { useExitOnCtrlCDWithKeybindings } from '../../hooks/useExitOnCtrlCDWithKeybindings.js'
import { useTerminalSize } from '../../hooks/useTerminalSize.js'
import { setClipboard } from '@anthropic/ink'
// eslint-disable-next-line custom-rules/prefer-use-keybindings -- raw j/k/arrow menu navigation
import { Box, color, Link, Text, useInput, useTheme } from '@anthropic/ink'
import { useKeybinding } from '../../keybindings/useKeybinding.js'
import {
AuthenticationCancelledError,
performMCPOAuthFlow,
revokeServerTokens,
} from '../../services/mcp/auth.js'
import { clearServerCache } from '../../services/mcp/client.js'
import {
useMcpReconnect,
useMcpToggleEnabled,
} from '../../services/mcp/MCPConnectionManager.js'
import {
describeMcpConfigFilePath,
excludeCommandsByServer,
excludeResourcesByServer,
excludeToolsByServer,
filterMcpPromptsByServer,
} from '../../services/mcp/utils.js'
import { useAppState, useSetAppState } from '../../state/AppState.js'
import { getOauthAccountInfo } from '../../utils/auth.js'
import { openBrowser } from '../../utils/browser.js'
import { errorMessage } from '../../utils/errors.js'
import { logMCPDebug } from '../../utils/log.js'
import { capitalize } from '../../utils/stringUtils.js'
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
import { Select } from '../CustomSelect/index.js'
import { Byline, KeyboardShortcutHint } from '@anthropic/ink'
import { Spinner } from '../Spinner.js'
import TextInput from '../TextInput.js'
import { CapabilitiesSection } from './CapabilitiesSection.js'
import type {
ClaudeAIServerInfo,
HTTPServerInfo,
SSEServerInfo,
} from './types.js'
import {
handleReconnectError,
handleReconnectResult,
} from './utils/reconnectHelpers.js'
type Props = {
server: SSEServerInfo | HTTPServerInfo | ClaudeAIServerInfo
serverToolsCount: number
onViewTools: () => void
onCancel: () => void
onComplete?: (
result?: string,
options?: { display?: CommandResultDisplay },
) => void
borderless?: boolean
}
export function MCPRemoteServerMenu({
server,
serverToolsCount,
onViewTools,
onCancel,
onComplete,
borderless = false,
}: Props): React.ReactNode {
const [theme] = useTheme()
const exitState = useExitOnCtrlCDWithKeybindings()
const { columns: terminalColumns } = useTerminalSize()
const [isAuthenticating, setIsAuthenticating] = React.useState(false)
const [error, setError] = React.useState<string | null>(null)
const mcp = useAppState(s => s.mcp)
const setAppState = useSetAppState()
const [authorizationUrl, setAuthorizationUrl] = React.useState<string | null>(
null,
)
const [isReconnecting, setIsReconnecting] = useState(false)
const authAbortControllerRef = useRef<AbortController | null>(null)
const [isClaudeAIAuthenticating, setIsClaudeAIAuthenticating] =
useState(false)
const [claudeAIAuthUrl, setClaudeAIAuthUrl] = useState<string | null>(null)
const [isClaudeAIClearingAuth, setIsClaudeAIClearingAuth] = useState(false)
const [claudeAIClearAuthUrl, setClaudeAIClearAuthUrl] = useState<
string | null
>(null)
const [claudeAIClearAuthBrowserOpened, setClaudeAIClearAuthBrowserOpened] =
useState(false)
const [urlCopied, setUrlCopied] = useState(false)
const copyTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(
undefined,
)
const unmountedRef = useRef(false)
const [callbackUrlInput, setCallbackUrlInput] = useState('')
const [callbackUrlCursorOffset, setCallbackUrlCursorOffset] = useState(0)
const [manualCallbackSubmit, setManualCallbackSubmit] = useState<
((url: string) => void) | null
>(null)
// If the component unmounts mid-auth (e.g. a parent component's Esc handler
// navigates away before ours fires), abort the OAuth flow so the callback
// server is closed. Without this, the server stays bound and the process
// can outlive the terminal. Also clear the copy-feedback timer and mark
// unmounted so the async setClipboard callback doesn't setUrlCopied /
// schedule a new timer after unmount.
useEffect(
() => () => {
unmountedRef.current = true
authAbortControllerRef.current?.abort()
if (copyTimeoutRef.current !== undefined) {
clearTimeout(copyTimeoutRef.current)
}
},
[],
)
// A server is effectively authenticated if:
// 1. It has OAuth tokens (server.isAuthenticated), OR
// 2. It's connected and has tools (meaning it's working via some auth mechanism)
const isEffectivelyAuthenticated =
server.isAuthenticated ||
(server.client.type === 'connected' && serverToolsCount > 0)
const reconnectMcpServer = useMcpReconnect()
const handleClaudeAIAuthComplete = React.useCallback(async () => {
setIsClaudeAIAuthenticating(false)
setClaudeAIAuthUrl(null)
setIsReconnecting(true)
try {
const result = await reconnectMcpServer(server.name)
const success = result.client.type === 'connected'
logEvent('tengu_claudeai_mcp_auth_completed', { success })
if (success) {
onComplete?.(`Authentication successful. Connected to ${server.name}.`)
} else if (result.client.type === 'needs-auth') {
onComplete?.(
'Authentication successful, but server still requires authentication. You may need to manually restart Claude Code.',
)
} else {
onComplete?.(
'Authentication successful, but server reconnection failed. You may need to manually restart Claude Code for the changes to take effect.',
)
}
} catch (err) {
logEvent('tengu_claudeai_mcp_auth_completed', { success: false })
onComplete?.(handleReconnectError(err, server.name))
} finally {
setIsReconnecting(false)
}
}, [reconnectMcpServer, server.name, onComplete])
const handleClaudeAIClearAuthComplete = React.useCallback(async () => {
await clearServerCache(server.name, {
...server.config,
scope: server.scope,
})
setAppState(prev => {
const newClients = prev.mcp.clients.map(c =>
c.name === server.name ? { ...c, type: 'needs-auth' as const } : c,
)
const newTools = excludeToolsByServer(prev.mcp.tools, server.name)
const newCommands = excludeCommandsByServer(
prev.mcp.commands,
server.name,
)
const newResources = excludeResourcesByServer(
prev.mcp.resources,
server.name,
)
return {
...prev,
mcp: {
...prev.mcp,
clients: newClients,
tools: newTools,
commands: newCommands,
resources: newResources,
},
}
})
logEvent('tengu_claudeai_mcp_clear_auth_completed', {})
onComplete?.(`Disconnected from ${server.name}.`)
setIsClaudeAIClearingAuth(false)
setClaudeAIClearAuthUrl(null)
setClaudeAIClearAuthBrowserOpened(false)
}, [server.name, server.config, server.scope, setAppState, onComplete])
// Escape to cancel authentication flow
useKeybinding(
'confirm:no',
() => {
authAbortControllerRef.current?.abort()
authAbortControllerRef.current = null
setIsAuthenticating(false)
setAuthorizationUrl(null)
},
{
context: 'Confirmation',
isActive: isAuthenticating,
},
)
// Escape to cancel Claude AI authentication
useKeybinding(
'confirm:no',
() => {
setIsClaudeAIAuthenticating(false)
setClaudeAIAuthUrl(null)
},
{
context: 'Confirmation',
isActive: isClaudeAIAuthenticating,
},
)
// Escape to cancel Claude AI clear auth
useKeybinding(
'confirm:no',
() => {
setIsClaudeAIClearingAuth(false)
setClaudeAIClearAuthUrl(null)
setClaudeAIClearAuthBrowserOpened(false)
},
{
context: 'Confirmation',
isActive: isClaudeAIClearingAuth,
},
)
// Return key handling for authentication flows and 'c' to copy URL
useInput((input, key) => {
if (key.return && isClaudeAIAuthenticating) {
void handleClaudeAIAuthComplete()
}
if (key.return && isClaudeAIClearingAuth) {
if (claudeAIClearAuthBrowserOpened) {
void handleClaudeAIClearAuthComplete()
} else {
// First Enter: open the browser
const connectorsUrl = `${getOauthConfig().CLAUDE_AI_ORIGIN}/settings/connectors`
setClaudeAIClearAuthUrl(connectorsUrl)
setClaudeAIClearAuthBrowserOpened(true)
void openBrowser(connectorsUrl)
}
}
if (input === 'c' && !urlCopied) {
const urlToCopy =
authorizationUrl || claudeAIAuthUrl || claudeAIClearAuthUrl
if (urlToCopy) {
void setClipboard(urlToCopy).then(raw => {
if (unmountedRef.current) return
if (raw) process.stdout.write(raw)
setUrlCopied(true)
if (copyTimeoutRef.current !== undefined) {
clearTimeout(copyTimeoutRef.current)
}
copyTimeoutRef.current = setTimeout(setUrlCopied, 2000, false)
})
}
}
})
const capitalizedServerName = capitalize(String(server.name))
// Count MCP prompts for this server (skills are shown in /skills, not here)
const serverCommandsCount = filterMcpPromptsByServer(
mcp.commands,
server.name,
).length
const toggleMcpServer = useMcpToggleEnabled()
const handleClaudeAIAuth = React.useCallback(async () => {
const claudeAiBaseUrl = getOauthConfig().CLAUDE_AI_ORIGIN
const accountInfo = getOauthAccountInfo()
const orgUuid = accountInfo?.organizationUuid
let authUrl: string
if (
orgUuid &&
server.config.type === 'claudeai-proxy' &&
server.config.id
) {
// Use the direct auth URL with org and server IDs
// Replace 'mcprs' prefix with 'mcpsrv' if present
const serverId = server.config.id.startsWith('mcprs')
? 'mcpsrv' + server.config.id.slice(5)
: server.config.id
const productSurface = encodeURIComponent(
process.env.CLAUDE_CODE_ENTRYPOINT || 'cli',
)
authUrl = `${claudeAiBaseUrl}/api/organizations/${orgUuid}/mcp/start-auth/${serverId}?product_surface=${productSurface}`
} else {
// Fall back to settings/connectors if we don't have the required IDs
authUrl = `${claudeAiBaseUrl}/settings/connectors`
}
setClaudeAIAuthUrl(authUrl)
setIsClaudeAIAuthenticating(true)
logEvent('tengu_claudeai_mcp_auth_started', {})
await openBrowser(authUrl)
}, [server.config])
const handleClaudeAIClearAuth = React.useCallback(() => {
setIsClaudeAIClearingAuth(true)
logEvent('tengu_claudeai_mcp_clear_auth_started', {})
}, [])
const handleToggleEnabled = React.useCallback(async () => {
const wasEnabled = server.client.type !== 'disabled'
try {
await toggleMcpServer(server.name)
if (server.config.type === 'claudeai-proxy') {
logEvent('tengu_claudeai_mcp_toggle', {
new_state: (wasEnabled
? 'disabled'
: 'enabled') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
})
}
// Return to the server list so user can continue managing other servers
onCancel()
} catch (err) {
const action = wasEnabled ? 'disable' : 'enable'
onComplete?.(
`Failed to ${action} MCP server '${server.name}': ${errorMessage(err)}`,
)
}
}, [
server.client.type,
server.config.type,
server.name,
toggleMcpServer,
onCancel,
onComplete,
])
const handleAuthenticate = React.useCallback(async () => {
if (server.config.type === 'claudeai-proxy') return
setIsAuthenticating(true)
setError(null)
const controller = new AbortController()
authAbortControllerRef.current = controller
try {
// Revoke existing tokens if re-authenticating, but preserve step-up
// auth state so the next OAuth flow can reuse cached scope/discovery.
if (server.isAuthenticated && server.config) {
await revokeServerTokens(server.name, server.config, {
preserveStepUpState: true,
})
}
if (server.config) {
await performMCPOAuthFlow(
server.name,
server.config,
setAuthorizationUrl,
controller.signal,
{
onWaitingForCallback: submit => {
setManualCallbackSubmit(() => submit)
},
},
)
logEvent('tengu_mcp_auth_config_authenticate', {
wasAuthenticated: server.isAuthenticated,
})
const result = await reconnectMcpServer(server.name)
if (result.client.type === 'connected') {
const message = isEffectivelyAuthenticated
? `Authentication successful. Reconnected to ${server.name}.`
: `Authentication successful. Connected to ${server.name}.`
onComplete?.(message)
} else if (result.client.type === 'needs-auth') {
onComplete?.(
'Authentication successful, but server still requires authentication. You may need to manually restart Claude Code.',
)
} else {
// result.client.type === 'failed'
logMCPDebug(server.name, `Reconnection failed after authentication`)
onComplete?.(
'Authentication successful, but server reconnection failed. You may need to manually restart Claude Code for the changes to take effect.',
)
}
}
} catch (err) {
// Don't show error if it was a cancellation
if (
err instanceof Error &&
!(err instanceof AuthenticationCancelledError)
) {
setError(err.message)
}
} finally {
setIsAuthenticating(false)
authAbortControllerRef.current = null
setManualCallbackSubmit(null)
setCallbackUrlInput('')
}
}, [
server.isAuthenticated,
server.config,
server.name,
onComplete,
reconnectMcpServer,
isEffectivelyAuthenticated,
])
const handleClearAuth = async () => {
if (server.config.type === 'claudeai-proxy') return
if (server.config) {
// First revoke the authentication tokens and clear all auth state
await revokeServerTokens(server.name, server.config)
logEvent('tengu_mcp_auth_config_clear', {})
// Disconnect the client and clear the cache
await clearServerCache(server.name, {
...server.config,
scope: server.scope,
})
// Update app state to remove the disconnected server's tools, commands, and resources
setAppState(prev => {
const newClients = prev.mcp.clients.map(c =>
// 'failed' is a misnomer here, but we don't really differentiate between "not connected" and "failed" at the moment
c.name === server.name ? { ...c, type: 'failed' as const } : c,
)
const newTools = excludeToolsByServer(prev.mcp.tools, server.name)
const newCommands = excludeCommandsByServer(
prev.mcp.commands,
server.name,
)
const newResources = excludeResourcesByServer(
prev.mcp.resources,
server.name,
)
return {
...prev,
mcp: {
...prev.mcp,
clients: newClients,
tools: newTools,
commands: newCommands,
resources: newResources,
},
}
})
onComplete?.(`Authentication cleared for ${server.name}.`)
}
}
if (isAuthenticating) {
// XAA: silent exchange (cached id_token → no browser), so don't claim
// one will open. If IdP login IS needed, authorizationUrl populates and
// the URL fallback block below still renders.
const authCopy =
server.config.type !== 'claudeai-proxy' && server.config.oauth?.xaa
? ' Authenticating via your identity provider'
: ' A browser window will open for authentication'
return (
<Box flexDirection="column" gap={1} padding={1}>
<Text color="claude">Authenticating with {server.name}</Text>
<Box>
<Spinner />
<Text>{authCopy}</Text>
</Box>
{authorizationUrl && (
<Box flexDirection="column">
<Box>
<Text dimColor>
If your browser doesn&apos;t open automatically, copy this URL
manually{' '}
</Text>
{urlCopied ? (
<Text color="success">(Copied!)</Text>
) : (
<Text dimColor>
<KeyboardShortcutHint shortcut="c" action="copy" parens />
</Text>
)}
</Box>
<Link url={authorizationUrl} />
</Box>
)}
{isAuthenticating && authorizationUrl && manualCallbackSubmit && (
<Box flexDirection="column" marginTop={1}>
<Text dimColor>
If the redirect page shows a connection error, paste the URL from
your browser&apos;s address bar:
</Text>
<Box>
<Text dimColor>URL {'>'} </Text>
<TextInput
value={callbackUrlInput}
onChange={setCallbackUrlInput}
onSubmit={(value: string) => {
manualCallbackSubmit(value.trim())
setCallbackUrlInput('')
}}
cursorOffset={callbackUrlCursorOffset}
onChangeCursorOffset={setCallbackUrlCursorOffset}
columns={terminalColumns - 8}
/>
</Box>
</Box>
)}
<Box marginLeft={3}>
<Text dimColor>
Return here after authenticating in your browser. Press Esc to go
back.
</Text>
</Box>
</Box>
)
}
if (isClaudeAIAuthenticating) {
return (
<Box flexDirection="column" gap={1} padding={1}>
<Text color="claude">Authenticating with {server.name}</Text>
<Box>
<Spinner />
<Text> A browser window will open for authentication</Text>
</Box>
{claudeAIAuthUrl && (
<Box flexDirection="column">
<Box>
<Text dimColor>
If your browser doesn&apos;t open automatically, copy this URL
manually{' '}
</Text>
{urlCopied ? (
<Text color="success">(Copied!)</Text>
) : (
<Text dimColor>
<KeyboardShortcutHint shortcut="c" action="copy" parens />
</Text>
)}
</Box>
<Link url={claudeAIAuthUrl} />
</Box>
)}
<Box marginLeft={3} flexDirection="column">
<Text color="permission">
Press <Text bold>Enter</Text> after authenticating in your browser.
</Text>
<Text dimColor italic>
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="back"
/>
</Text>
</Box>
</Box>
)
}
if (isClaudeAIClearingAuth) {
return (
<Box flexDirection="column" gap={1} padding={1}>
<Text color="claude">Clear authentication for {server.name}</Text>
{claudeAIClearAuthBrowserOpened ? (
<>
<Text>
Find the MCP server in the browser and click
&quot;Disconnect&quot;.
</Text>
{claudeAIClearAuthUrl && (
<Box flexDirection="column">
<Box>
<Text dimColor>
If your browser didn&apos;t open automatically, copy this
URL manually{' '}
</Text>
{urlCopied ? (
<Text color="success">(Copied!)</Text>
) : (
<Text dimColor>
<KeyboardShortcutHint shortcut="c" action="copy" parens />
</Text>
)}
</Box>
<Link url={claudeAIClearAuthUrl} />
</Box>
)}
<Box marginLeft={3} flexDirection="column">
<Text color="permission">
Press <Text bold>Enter</Text> when done.
</Text>
<Text dimColor italic>
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="back"
/>
</Text>
</Box>
</>
) : (
<>
<Text>
This will open claude.ai in the browser. Find the MCP server in
the list and click &quot;Disconnect&quot;.
</Text>
<Box marginLeft={3} flexDirection="column">
<Text color="permission">
Press <Text bold>Enter</Text> to open the browser.
</Text>
<Text dimColor italic>
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="back"
/>
</Text>
</Box>
</>
)}
</Box>
)
}
if (isReconnecting) {
return (
<Box flexDirection="column" gap={1} padding={1}>
<Text color="text">
Connecting to <Text bold>{server.name}</Text>
</Text>
<Box>
<Spinner />
<Text> Establishing connection to MCP server</Text>
</Box>
<Text dimColor>This may take a few moments.</Text>
</Box>
)
}
const menuOptions = []
// If server is disabled, show Enable first as the primary action
if (server.client.type === 'disabled') {
menuOptions.push({
label: 'Enable',
value: 'toggle-enabled',
})
}
if (server.client.type === 'connected' && serverToolsCount > 0) {
menuOptions.push({
label: 'View tools',
value: 'tools',
})
}
if (server.config.type === 'claudeai-proxy') {
if (server.client.type === 'connected') {
menuOptions.push({
label: 'Clear authentication',
value: 'claudeai-clear-auth',
})
} else if (server.client.type !== 'disabled') {
menuOptions.push({
label: 'Authenticate',
value: 'claudeai-auth',
})
}
} else {
if (isEffectivelyAuthenticated) {
menuOptions.push({
label: 'Re-authenticate',
value: 'reauth',
})
menuOptions.push({
label: 'Clear authentication',
value: 'clear-auth',
})
}
if (!isEffectivelyAuthenticated) {
menuOptions.push({
label: 'Authenticate',
value: 'auth',
})
}
}
if (server.client.type !== 'disabled') {
if (server.client.type !== 'needs-auth') {
menuOptions.push({
label: 'Reconnect',
value: 'reconnectMcpServer',
})
}
menuOptions.push({
label: 'Disable',
value: 'toggle-enabled',
})
}
// If there are no other options, add a back option so Select handles escape
if (menuOptions.length === 0) {
menuOptions.push({
label: 'Back',
value: 'back',
})
}
return (
<Box flexDirection="column">
<Box
flexDirection="column"
paddingX={1}
borderStyle={borderless ? undefined : 'round'}
>
<Box marginBottom={1}>
<Text bold>{capitalizedServerName} MCP Server</Text>
</Box>
<Box flexDirection="column" gap={0}>
<Box>
<Text bold>Status: </Text>
{server.client.type === 'disabled' ? (
<Text>{color('inactive', theme)(figures.radioOff)} disabled</Text>
) : server.client.type === 'connected' ? (
<Text>{color('success', theme)(figures.tick)} connected</Text>
) : server.client.type === 'pending' ? (
<>
<Text dimColor>{figures.radioOff}</Text>
<Text> connecting</Text>
</>
) : server.client.type === 'needs-auth' ? (
<Text>
{color('warning', theme)(figures.triangleUpOutline)} needs
authentication
</Text>
) : (
<Text>{color('error', theme)(figures.cross)} failed</Text>
)}
</Box>
{server.transport !== 'claudeai-proxy' && (
<Box>
<Text bold>Auth: </Text>
{isEffectivelyAuthenticated ? (
<Text>
{color('success', theme)(figures.tick)} authenticated
</Text>
) : (
<Text>
{color('error', theme)(figures.cross)} not authenticated
</Text>
)}
</Box>
)}
<Box>
<Text bold>URL: </Text>
<Text dimColor>{server.config.url}</Text>
</Box>
<Box>
<Text bold>Config location: </Text>
<Text dimColor>{describeMcpConfigFilePath(server.scope)}</Text>
</Box>
{server.client.type === 'connected' && (
<CapabilitiesSection
serverToolsCount={serverToolsCount}
serverPromptsCount={serverCommandsCount}
serverResourcesCount={mcp.resources[server.name]?.length || 0}
/>
)}
{server.client.type === 'connected' && serverToolsCount > 0 && (
<Box>
<Text bold>Tools: </Text>
<Text dimColor>{serverToolsCount} tools</Text>
</Box>
)}
</Box>
{error && (
<Box marginTop={1}>
<Text color="error">Error: {error}</Text>
</Box>
)}
{menuOptions.length > 0 && (
<Box marginTop={1}>
<Select
options={menuOptions}
onChange={async value => {
switch (value) {
case 'tools':
onViewTools()
break
case 'auth':
case 'reauth':
await handleAuthenticate()
break
case 'clear-auth':
await handleClearAuth()
break
case 'claudeai-auth':
await handleClaudeAIAuth()
break
case 'claudeai-clear-auth':
handleClaudeAIClearAuth()
break
case 'reconnectMcpServer':
setIsReconnecting(true)
try {
const result = await reconnectMcpServer(server.name)
if (server.config.type === 'claudeai-proxy') {
logEvent('tengu_claudeai_mcp_reconnect', {
success: result.client.type === 'connected',
})
}
const { message } = handleReconnectResult(
result,
server.name,
)
onComplete?.(message)
} catch (err) {
if (server.config.type === 'claudeai-proxy') {
logEvent('tengu_claudeai_mcp_reconnect', {
success: false,
})
}
onComplete?.(handleReconnectError(err, server.name))
} finally {
setIsReconnecting(false)
}
break
case 'toggle-enabled':
await handleToggleEnabled()
break
case 'back':
onCancel()
break
}
}}
onCancel={onCancel}
/>
</Box>
)}
</Box>
<Box marginTop={1}>
<Text dimColor italic>
{exitState.pending ? (
<>Press {exitState.keyName} again to exit</>
) : (
<Byline>
<KeyboardShortcutHint shortcut="↑↓" action="navigate" />
<KeyboardShortcutHint shortcut="Enter" action="select" />
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="back"
/>
</Byline>
)}
</Text>
</Box>
</Box>
)
}