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

193 lines
5.5 KiB
TypeScript

import figures from 'figures'
import * as React from 'react'
import { Suspense, use } from 'react'
import { getSessionId } from '../../bootstrap/state.js'
import type { LocalJSXCommandContext } from '../../commands.js'
import { useIsInsideModal } from '../../context/modalContext.js'
import { Box, Text, useTheme } from '@anthropic/ink'
import { type AppState, useAppState } from '../../state/AppState.js'
import { getCwd } from '../../utils/cwd.js'
import { getCurrentSessionTitle } from '../../utils/sessionStorage.js'
import {
buildAccountProperties,
buildAPIProviderProperties,
buildIDEProperties,
buildInstallationDiagnostics,
buildInstallationHealthDiagnostics,
buildMcpProperties,
buildMemoryDiagnostics,
buildSandboxProperties,
buildSettingSourcesProperties,
type Diagnostic,
getModelDisplayLabel,
type Property,
} from '../../utils/status.js'
import type { ThemeName } from '../../utils/theme.js'
import { ConfigurableShortcutHint } from '../ConfigurableShortcutHint.js'
type Props = {
context: LocalJSXCommandContext
diagnosticsPromise: Promise<Diagnostic[]>
}
function buildPrimarySection(): Property[] {
const sessionId = getSessionId()
const customTitle = getCurrentSessionTitle(sessionId)
const nameValue = customTitle ?? <Text dimColor>/rename to add a name</Text>
return [
{ label: 'Version', value: MACRO.VERSION },
{ label: 'Session name', value: nameValue },
{ label: 'Session ID', value: sessionId },
{ label: 'cwd', value: getCwd() },
...buildAccountProperties(),
...buildAPIProviderProperties(),
]
}
function buildSecondarySection({
mainLoopModel,
mcp,
theme,
context,
}: {
mainLoopModel: AppState['mainLoopModel']
mcp: AppState['mcp']
theme: ThemeName
context: LocalJSXCommandContext
}): Property[] {
const modelLabel = getModelDisplayLabel(mainLoopModel)
return [
{ label: 'Model', value: modelLabel },
...buildIDEProperties(
mcp.clients,
context.options.ideInstallationStatus,
theme,
),
...buildMcpProperties(mcp.clients, theme),
...buildSandboxProperties(),
...buildSettingSourcesProperties(),
]
}
export async function buildDiagnostics(): Promise<Diagnostic[]> {
return [
...(await buildInstallationDiagnostics()),
...(await buildInstallationHealthDiagnostics()),
...(await buildMemoryDiagnostics()),
]
}
function PropertyValue({
value,
}: {
value: Property['value']
}): React.ReactNode {
if (Array.isArray(value)) {
return (
<Box flexWrap="wrap" columnGap={1} flexShrink={99}>
{value.map((item, i) => {
return (
<Text key={i}>
{item}
{i < value.length - 1 ? ',' : ''}
</Text>
)
})}
</Box>
)
}
if (typeof value === 'string') {
return <Text>{value}</Text>
}
return value
}
export function Status({
context,
diagnosticsPromise,
}: Props): React.ReactNode {
const mainLoopModel = useAppState(s => s.mainLoopModel)
const mcp = useAppState(s => s.mcp)
const [theme] = useTheme()
// Sections are synchronous — compute in render so they're never empty.
// diagnosticsPromise is created once in Settings.tsx so it resolves once
// per pane invocation instead of re-fetching on every tab switch (Tab
// unmounts children when not selected, which was causing the flash).
const sections = React.useMemo(
() => [
buildPrimarySection(),
buildSecondarySection({ mainLoopModel, mcp, theme, context }),
],
[mainLoopModel, mcp, theme, context],
)
// flexGrow so the "Esc to cancel" footer pins to the bottom of the
// Modal's inner ScrollBox when content is short. The ScrollBox content
// wrapper has flexGrow:1 (fills at least the viewport), so this stretches
// to match. Without it, short Status content floats at the top and the
// footer sits mid-modal with 2-3 trailing blank rows below. Outside a
// Modal (non-fullscreen), leave layout alone — no ScrollBox to fill.
const grow = useIsInsideModal() ? 1 : undefined
return (
<Box flexDirection="column" flexGrow={grow}>
<Box flexDirection="column" gap={1} flexGrow={grow}>
{sections.map(
(properties, i) =>
properties.length > 0 && (
<Box key={i} flexDirection="column">
{properties.map(({ label, value }, j) => (
<Box key={j} flexDirection="row" gap={1} flexShrink={0}>
{label !== undefined && <Text bold>{label}:</Text>}
<PropertyValue value={value} />
</Box>
))}
</Box>
),
)}
<Suspense fallback={null}>
<Diagnostics promise={diagnosticsPromise} />
</Suspense>
</Box>
<Text dimColor>
<ConfigurableShortcutHint
action="confirm:no"
context="Settings"
fallback="Esc"
description="cancel"
/>
</Text>
</Box>
)
}
function Diagnostics({
promise,
}: {
promise: Promise<Diagnostic[]>
}): React.ReactNode {
const diagnostics = use(promise)
if (diagnostics.length === 0) return null
return (
<Box flexDirection="column" paddingBottom={1}>
<Text bold>System Diagnostics</Text>
{diagnostics.map((diagnostic, i) => (
<Box key={i} flexDirection="row" gap={1} paddingX={1}>
<Text color="error">{figures.warning}</Text>
{typeof diagnostic === 'string' ? (
<Text wrap="wrap">{diagnostic}</Text>
) : (
diagnostic
)}
</Box>
))}
</Box>
)
}