Files
claude-code/src/components/RemoteEnvironmentDialog.tsx
2026-04-07 16:17:48 +08:00

235 lines
6.4 KiB
TypeScript

import chalk from 'chalk'
import figures from 'figures'
import * as React from 'react'
import { useEffect, useState } from 'react'
import { Text } from '@anthropic/ink'
import { useKeybinding } from '../keybindings/useKeybinding.js'
import { toError } from '../utils/errors.js'
import { logError } from '../utils/log.js'
import {
getSettingSourceName,
type SettingSource,
} from '../utils/settings/constants.js'
import { updateSettingsForSource } from '../utils/settings/settings.js'
import { getEnvironmentSelectionInfo } from '../utils/teleport/environmentSelection.js'
import type { EnvironmentResource } from '../utils/teleport/environments.js'
import { ConfigurableShortcutHint } from './ConfigurableShortcutHint.js'
import { Select } from './CustomSelect/select.js'
import { Byline, Dialog, KeyboardShortcutHint, LoadingState } from '@anthropic/ink'
const DIALOG_TITLE = 'Select Remote Environment'
const SETUP_HINT = `Configure environments at: https://claude.ai/code`
type Props = {
onDone: (message?: string) => void
}
type LoadingState = 'loading' | 'updating' | null
export function RemoteEnvironmentDialog({ onDone }: Props): React.ReactNode {
const [loadingState, setLoadingState] = useState<LoadingState>('loading')
const [environments, setEnvironments] = useState<EnvironmentResource[]>([])
const [selectedEnvironment, setSelectedEnvironment] =
useState<EnvironmentResource | null>(null)
const [selectedEnvironmentSource, setSelectedEnvironmentSource] =
useState<SettingSource | null>(null)
const [error, setError] = useState<string | null>(null)
useEffect(() => {
let cancelled = false
async function fetchInfo(): Promise<void> {
try {
const result = await getEnvironmentSelectionInfo()
if (cancelled) return
setEnvironments(result.availableEnvironments)
setSelectedEnvironment(result.selectedEnvironment)
setSelectedEnvironmentSource(result.selectedEnvironmentSource)
setLoadingState(null)
} catch (err) {
if (cancelled) return
const fetchError = toError(err)
logError(fetchError)
setError(fetchError.message)
setLoadingState(null)
}
}
void fetchInfo()
return () => {
cancelled = true
}
}, [])
function handleSelect(value: string): void {
if (value === 'cancel') {
onDone()
return
}
setLoadingState('updating')
const selectedEnv = environments.find(env => env.environment_id === value)
if (!selectedEnv) {
onDone('Error: Selected environment not found')
return
}
updateSettingsForSource('localSettings', {
remote: {
defaultEnvironmentId: selectedEnv.environment_id,
},
})
onDone(
`Set default remote environment to ${chalk.bold(selectedEnv.name)} (${selectedEnv.environment_id})`,
)
}
// Loading state
if (loadingState === 'loading') {
return (
<Dialog title={DIALOG_TITLE} onCancel={onDone} hideInputGuide>
<LoadingState message="Loading environments…" />
</Dialog>
)
}
// Error state
if (error) {
return (
<Dialog title={DIALOG_TITLE} onCancel={onDone}>
<Text color="error">Error: {error}</Text>
</Dialog>
)
}
// No environments available
if (!selectedEnvironment) {
return (
<Dialog title={DIALOG_TITLE} subtitle={SETUP_HINT} onCancel={onDone}>
<Text>No remote environments available.</Text>
</Dialog>
)
}
// Single environment - just show info
if (environments.length === 1) {
return (
<SingleEnvironmentContent
environment={selectedEnvironment}
onDone={onDone}
/>
)
}
// Multiple environments - show selection UI
return (
<MultipleEnvironmentsContent
environments={environments}
selectedEnvironment={selectedEnvironment}
selectedEnvironmentSource={selectedEnvironmentSource}
loadingState={loadingState}
onSelect={handleSelect}
onCancel={onDone}
/>
)
}
function EnvironmentLabel({
environment,
}: {
environment: EnvironmentResource
}): React.ReactNode {
return (
<Text>
{figures.tick} Using <Text bold>{environment.name}</Text>{' '}
<Text dimColor>({environment.environment_id})</Text>
</Text>
)
}
function SingleEnvironmentContent({
environment,
onDone,
}: {
environment: EnvironmentResource
onDone: () => void
}): React.ReactNode {
// Handle Enter to continue
useKeybinding('confirm:yes', onDone, { context: 'Confirmation' })
return (
<Dialog title={DIALOG_TITLE} subtitle={SETUP_HINT} onCancel={onDone}>
<EnvironmentLabel environment={environment} />
</Dialog>
)
}
function MultipleEnvironmentsContent({
environments,
selectedEnvironment,
selectedEnvironmentSource,
loadingState,
onSelect,
onCancel,
}: {
environments: EnvironmentResource[]
selectedEnvironment: EnvironmentResource
selectedEnvironmentSource: SettingSource | null
loadingState: LoadingState
onSelect: (value: string) => void
onCancel: () => void
}): React.ReactNode {
const sourceSuffix =
selectedEnvironmentSource && selectedEnvironmentSource !== 'localSettings'
? ` (from ${getSettingSourceName(selectedEnvironmentSource)} settings)`
: ''
const subtitle = (
<Text>
Currently using: <Text bold>{selectedEnvironment.name}</Text>
{sourceSuffix}
</Text>
)
return (
<Dialog
title={DIALOG_TITLE}
subtitle={subtitle}
onCancel={onCancel}
hideInputGuide
>
<Text dimColor>{SETUP_HINT}</Text>
{loadingState === 'updating' ? (
<LoadingState message="Updating…" />
) : (
<Select
options={environments.map(env => ({
label: (
<Text>
{env.name} <Text dimColor>({env.environment_id})</Text>
</Text>
),
value: env.environment_id,
}))}
defaultValue={selectedEnvironment.environment_id}
onChange={onSelect}
onCancel={() => onSelect('cancel')}
layout="compact-vertical"
/>
)}
<Text dimColor>
<Byline>
<KeyboardShortcutHint shortcut="Enter" action="select" />
<ConfigurableShortcutHint
action="confirm:no"
context="Confirmation"
fallback="Esc"
description="cancel"
/>
</Byline>
</Text>
</Dialog>
)
}