inspector/clients/tui/src/components/InfoTab.tsx
Cliff Hall 373f346e5f
Some checks failed
Run install, format, lint, build, and test on every push / build (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish (push) Has been cancelled
Run install, format, lint, build, and test on every push / publish-github-container-registry (push) Has been cancelled
chore: close quality-gate coverage gaps (#1689) (#1766)
Closes #1689. Typecheck cli/tui src, add a core/ format+lint gate, and gate core/json + core/client coverage. See PR #1766 for the full review history.
2026-07-24 17:20:12 -04:00

225 lines
8.1 KiB
TypeScript

import React, { useRef } from "react";
import { Box, Text, useInput, type Key } from "ink";
import { ScrollView, type ScrollViewRef } from "ink-scroll-view";
import type {
MCPServerConfig,
ServerState,
} from "@inspector/core/mcp/index.js";
import type { InspectorServerSettings } from "@inspector/core/mcp/types.js";
interface InfoTabProps {
serverName: string | null;
serverConfig: MCPServerConfig | null;
// HTTP headers live on the per-server settings (pair-array form), not on the
// transport `serverConfig` — `mcpConfigToServerEntries` lifts the on-disk
// `headers` map into `InspectorServerSettings.headers`. Read them here so the
// SSE / streamable-http header display reflects the real source.
serverSettings?: InspectorServerSettings | null;
serverState: ServerState | null;
width: number;
height: number;
focused?: boolean;
}
export function InfoTab({
serverName,
serverConfig,
serverSettings = null,
serverState,
width,
height,
focused = false,
}: InfoTabProps) {
const headerPairs = serverSettings?.headers ?? [];
// Shared header display for the sse / streamable-http branches (identical for
// both transports — the header source is `serverSettings`, not the transport).
const headersBlock =
headerPairs.length > 0 ? (
<Box marginTop={1}>
<Text dimColor>
Headers:{" "}
{headerPairs.map(({ key, value }) => `${key}=${value}`).join(", ")}
</Text>
</Box>
) : null;
const scrollViewRef = useRef<ScrollViewRef>(null);
// Handle keyboard input for scrolling
useInput(
(input: string, key: Key) => {
if (focused) {
if (key.upArrow) {
scrollViewRef.current?.scrollBy(-1);
} else if (key.downArrow) {
scrollViewRef.current?.scrollBy(1);
} else if (key.pageUp) {
const viewportHeight =
scrollViewRef.current?.getViewportHeight() || 1;
scrollViewRef.current?.scrollBy(-viewportHeight);
} else if (key.pageDown) {
const viewportHeight =
scrollViewRef.current?.getViewportHeight() || 1;
scrollViewRef.current?.scrollBy(viewportHeight);
}
}
},
{ isActive: focused },
);
return (
<Box width={width} height={height} flexDirection="column" paddingX={1}>
<Box paddingY={1} flexShrink={0}>
<Text bold backgroundColor={focused ? "yellow" : undefined}>
Info
</Text>
</Box>
{serverName ? (
<>
{/* Scrollable content area - takes remaining space */}
<Box height={height - 4} overflow="hidden" paddingTop={1}>
<ScrollView ref={scrollViewRef} height={height - 4}>
{/* Server Configuration */}
<Box flexShrink={0} marginTop={1}>
<Text bold>Server Configuration</Text>
</Box>
{serverConfig ? (
<Box
flexShrink={0}
marginTop={1}
paddingLeft={2}
flexDirection="column"
>
{serverConfig.type === undefined ||
serverConfig.type === "stdio" ? (
<>
<Text dimColor>Type: stdio</Text>
<Text dimColor>Command: {serverConfig.command}</Text>
{serverConfig.args && serverConfig.args.length > 0 && (
<Box marginTop={1} flexDirection="column">
<Text dimColor>Args:</Text>
{serverConfig.args.map((arg: string, idx: number) => (
<Box
key={`arg-${idx}`}
paddingLeft={2}
marginTop={idx === 0 ? 0 : 0}
>
<Text dimColor>{arg}</Text>
</Box>
))}
</Box>
)}
{serverConfig.env &&
Object.keys(serverConfig.env).length > 0 && (
<Box marginTop={1}>
<Text dimColor>
Env:{" "}
{Object.entries(serverConfig.env)
.map(([k, v]) => `${k}=${v}`)
.join(", ")}
</Text>
</Box>
)}
{serverConfig.cwd && (
<Box marginTop={1}>
<Text dimColor>CWD: {serverConfig.cwd}</Text>
</Box>
)}
</>
) : serverConfig.type === "sse" ? (
<>
<Text dimColor>Type: sse</Text>
<Text dimColor>URL: {serverConfig.url}</Text>
{headersBlock}
</>
) : serverConfig.type === "streamable-http" ? (
<>
<Text dimColor>Type: streamable-http</Text>
<Text dimColor>URL: {serverConfig.url}</Text>
{headersBlock}
</>
) : null}
</Box>
) : (
<Box marginTop={1} paddingLeft={2}>
<Text dimColor>No configuration available</Text>
</Box>
)}
{/* Server Info */}
{serverState &&
serverState.status === "connected" &&
serverState.serverInfo && (
<>
<Box flexShrink={0} marginTop={2}>
<Text bold>Server Information</Text>
</Box>
<Box
flexShrink={0}
marginTop={1}
paddingLeft={2}
flexDirection="column"
>
{serverState.serverInfo.name && (
<Text dimColor>
Name: {serverState.serverInfo.name}
</Text>
)}
{serverState.serverInfo.version && (
<Box marginTop={1}>
<Text dimColor>
Version: {serverState.serverInfo.version}
</Text>
</Box>
)}
{serverState.instructions && (
<Box marginTop={1} flexDirection="column">
<Text dimColor>Instructions:</Text>
<Box paddingLeft={2} marginTop={1}>
<Text dimColor>{serverState.instructions}</Text>
</Box>
</Box>
)}
</Box>
</>
)}
{serverState && serverState.status === "error" && (
<Box flexShrink={0} marginTop={2}>
<Text bold color="red">
Error
</Text>
{serverState.error && (
<Box marginTop={1} paddingLeft={2}>
<Text color="red">{serverState.error}</Text>
</Box>
)}
</Box>
)}
{serverState && serverState.status === "disconnected" && (
<Box flexShrink={0} marginTop={2}>
<Text dimColor>Server not connected</Text>
</Box>
)}
</ScrollView>
</Box>
{/* Fixed keyboard help footer at bottom - only show when focused */}
{focused && (
<Box
flexShrink={0}
height={1}
justifyContent="center"
backgroundColor="gray"
>
<Text bold color="white">
↑/↓ to scroll, + to zoom
</Text>
</Box>
)}
</>
) : null}
</Box>
);
}