refactor molecule components to use .withProps() subcomponents and extract inline code

- Add UnstyledButton listItem theme variant to replace inline style for border-radius
- Extract subcomponent constants with .withProps() across all molecules
- Replace raw <span /> with Mantine <Flex /> in ResourcePreviewPanel
- Extract inline template literals and expressions to named functions

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
cliffhall 2026-03-24 14:06:11 -04:00
commit 27186c2ff7
27 changed files with 666 additions and 356 deletions

View file

@ -12,6 +12,19 @@ export interface ElicitationFormPanelProps {
onCancel: () => void;
}
const QuotedMessage = Text.withProps({
size: "md",
fs: "italic",
});
function formatQuoted(text: string): string {
return `\u201C${text}\u201D`;
}
function formatWarning(serverName: string): string {
return `Only provide information you trust this server with. The server \u201C${serverName}\u201D is requesting this data.`;
}
export function ElicitationFormPanel({
message,
schema,
@ -23,14 +36,11 @@ export function ElicitationFormPanel({
}: ElicitationFormPanelProps) {
return (
<Stack gap="md">
<Text size="md" fs="italic">
&ldquo;{message}&rdquo;
</Text>
<QuotedMessage>{formatQuoted(message)}</QuotedMessage>
<Divider />
<SchemaForm schema={schema} values={values} onChange={onChange} />
<Alert color="yellow" title="Warning">
Only provide information you trust this server with. The server &ldquo;
{serverName}&rdquo; is requesting this data.
{formatWarning(serverName)}
</Alert>
<Group justify="flex-end">
<Button variant="light" onClick={onCancel}>

View file

@ -19,6 +19,30 @@ export interface ElicitationUrlPanelProps {
onCancel: () => void;
}
const ItalicMessage = Text.withProps({
size: "md",
fs: "italic",
});
const WrappingCode = Code.withProps({
block: true,
variant: "wrapping",
});
const HintText = Text.withProps({
size: "sm",
c: "dimmed",
});
const MetaText = Text.withProps({
size: "xs",
c: "dimmed",
});
function formatElicitationId(id: string): string {
return `Elicitation ID: ${id}`;
}
export function ElicitationUrlPanel({
message,
url,
@ -30,14 +54,10 @@ export function ElicitationUrlPanel({
}: ElicitationUrlPanelProps) {
return (
<Stack gap="md">
<Text size="md" fs="italic">
{message}
</Text>
<ItalicMessage>{message}</ItalicMessage>
<Divider />
<Text size="sm">The server is requesting you visit:</Text>
<Code block variant="wrapping">
{url}
</Code>
<WrappingCode>{url}</WrappingCode>
<Group>
<Button variant="light" onClick={onCopyUrl}>
Copy URL
@ -50,14 +70,10 @@ export function ElicitationUrlPanel({
{isWaiting && (
<Group>
<Loader size="sm" />
<Text size="sm" c="dimmed">
Waiting for completion...
</Text>
<HintText>Waiting for completion...</HintText>
</Group>
)}
<Text size="xs" c="dimmed">
Elicitation ID: {elicitationId}
</Text>
<MetaText>{formatElicitationId(elicitationId)}</MetaText>
<Alert color="yellow" title="Warning">
This will open an external URL. Verify the domain before proceeding.
</Alert>

View file

@ -54,6 +54,39 @@ export interface ExperimentalFeaturesPanelProps {
onTestCapability: (name: string) => void;
}
const HintText = Text.withProps({
size: "sm",
c: "dimmed",
});
const MetaText = Text.withProps({
size: "xs",
c: "dimmed",
});
const CapCard = Card.withProps({
withBorder: true,
p: "sm",
});
const CompactButton = Button.withProps({
size: "xs",
variant: "light",
});
const RemoveIcon = ActionIcon.withProps({
variant: "light",
color: "red",
});
function formatMethods(methods: string[]): string {
return `Methods: ${methods.join(", ")}`;
}
function formatDuration(ms: number): string {
return `${ms}ms`;
}
export function ExperimentalFeaturesPanel({
serverCapabilities,
clientCapabilities,
@ -82,30 +115,20 @@ export function ExperimentalFeaturesPanel({
<Text c="dimmed">No experimental capabilities</Text>
) : (
serverCapabilities.map((cap) => (
<Card key={cap.name} withBorder p="sm">
<CapCard key={cap.name}>
<Stack gap="xs">
<Text fw={600}>{cap.name}</Text>
{cap.description && (
<Text size="sm" c="dimmed">
{cap.description}
</Text>
)}
{cap.description && <HintText>{cap.description}</HintText>}
{cap.methods && cap.methods.length > 0 && (
<Text size="xs" c="dimmed">
Methods: {cap.methods.join(", ")}
</Text>
<MetaText>{formatMethods(cap.methods)}</MetaText>
)}
<Group>
<Button
size="xs"
variant="light"
onClick={() => onTestCapability(cap.name)}
>
<CompactButton onClick={() => onTestCapability(cap.name)}>
Test →
</Button>
</CompactButton>
</Group>
</Stack>
</Card>
</CapCard>
))
)}
@ -128,9 +151,7 @@ export function ExperimentalFeaturesPanel({
<Title order={5}>Advanced JSON-RPC Tester</Title>
<Text size="sm" c="dimmed">
Send raw JSON-RPC requests to test ANY method
</Text>
<HintText>Send raw JSON-RPC requests to test ANY method</HintText>
{customHeaders.map((header, index) => (
<Group key={index}>
@ -148,20 +169,14 @@ export function ExperimentalFeaturesPanel({
onHeaderChange(index, header.key, e.currentTarget.value)
}
/>
<ActionIcon
variant="light"
color="red"
onClick={() => onRemoveHeader(index)}
>
<RemoveIcon onClick={() => onRemoveHeader(index)}>
<Text size="xs">✕</Text>
</ActionIcon>
</RemoveIcon>
</Group>
))}
<Group>
<Button size="xs" variant="light" onClick={onAddHeader}>
+ Add Header
</Button>
<CompactButton onClick={onAddHeader}>+ Add Header</CompactButton>
</Group>
<Textarea
@ -186,9 +201,7 @@ export function ExperimentalFeaturesPanel({
minRows={4}
/>
<Group>
<Button variant="light" size="xs" onClick={onCopyResponse}>
Copy
</Button>
<CompactButton onClick={onCopyResponse}>Copy</CompactButton>
</Group>
</>
)}
@ -211,7 +224,7 @@ export function ExperimentalFeaturesPanel({
<Table.Td>{item.timestamp}</Table.Td>
<Table.Td>{item.method}</Table.Td>
<Table.Td>{item.status}</Table.Td>
<Table.Td>{item.durationMs}ms</Table.Td>
<Table.Td>{formatDuration(item.durationMs)}</Table.Td>
</Table.Tr>
))}
</Table.Tbody>

View file

@ -34,6 +34,63 @@ export interface HistoryEntryProps {
onTogglePin: () => void;
}
const EntryContainer = Card.withProps({
withBorder: true,
padding: "md",
});
const HeaderRow = Group.withProps({
justify: "space-between",
wrap: "nowrap",
});
const TimestampText = Text.withProps({
size: "sm",
c: "dimmed",
ff: "monospace",
});
const TargetText = Text.withProps({
size: "sm",
fw: 500,
});
const DurationText = Text.withProps({
size: "sm",
c: "dimmed",
});
const SubtleButton = Button.withProps({
variant: "subtle",
size: "xs",
});
const ChildMethodBadge = Badge.withProps({
color: "dark",
size: "sm",
});
function formatDuration(ms: number): string {
return `${ms}ms`;
}
function formatStatusLabel(status: "success" | "error"): string {
return status === "success" ? "OK" : "Error";
}
function statusColor(status: "success" | "error"): string {
return status === "success" ? "green" : "red";
}
function formatPinLabel(isPinned: boolean, pinLabel?: string): string {
const base = isPinned ? "Unpin" : "Pin";
return isPinned && pinLabel ? `${base} (${pinLabel})` : base;
}
function serializeJson(value: Record<string, unknown>): string {
return JSON.stringify(value);
}
export function HistoryEntry({
timestamp,
method,
@ -51,41 +108,30 @@ export function HistoryEntry({
onTogglePin,
}: HistoryEntryProps) {
return (
<Card withBorder padding="md">
<EntryContainer>
<Stack gap="sm">
<Group justify="space-between" wrap="nowrap">
<HeaderRow>
<Group gap="sm">
<Text size="sm" c="dimmed" ff="monospace">
{timestamp}
</Text>
<TimestampText>{timestamp}</TimestampText>
<Badge color="dark">{method}</Badge>
{target && (
<Text size="sm" fw={500}>
{target}
</Text>
)}
{target && <TargetText>{target}</TargetText>}
</Group>
<Group gap="sm">
<Text size="sm" c="dimmed">
{durationMs}ms
</Text>
<Badge color={status === "success" ? "green" : "red"}>
{status === "success" ? "OK" : "Error"}
<DurationText>{formatDuration(durationMs)}</DurationText>
<Badge color={statusColor(status)}>
{formatStatusLabel(status)}
</Badge>
</Group>
</Group>
</HeaderRow>
<Group gap="xs">
<Button variant="subtle" size="xs" onClick={onReplay}>
Replay
</Button>
<Button variant="subtle" size="xs" onClick={onTogglePin}>
{isPinned ? "Unpin" : "Pin"}
{isPinned && pinLabel ? ` (${pinLabel})` : ""}
</Button>
<Button variant="subtle" size="xs" onClick={onToggleExpand} ml="auto">
<SubtleButton onClick={onReplay}>Replay</SubtleButton>
<SubtleButton onClick={onTogglePin}>
{formatPinLabel(isPinned, pinLabel)}
</SubtleButton>
<SubtleButton onClick={onToggleExpand} ml="auto">
{isExpanded ? "Collapse" : "Expand"}
</Button>
</SubtleButton>
</Group>
<Collapse in={isExpanded}>
@ -95,7 +141,7 @@ export function HistoryEntry({
<Text size="sm">Parameters:</Text>
<ContentViewer
type="json"
content={JSON.stringify(parameters)}
content={serializeJson(parameters)}
copyable
/>
</Stack>
@ -105,7 +151,7 @@ export function HistoryEntry({
<Text size="sm">Response:</Text>
<ContentViewer
type="json"
content={JSON.stringify(response)}
content={serializeJson(response)}
copyable
/>
</Stack>
@ -114,25 +160,16 @@ export function HistoryEntry({
<Stack gap="xs">
{childEntries.map((child, index) => (
<Group key={index} pl="lg" gap="sm">
<Text size="sm" c="dimmed" ff="monospace">
+--{" "}
</Text>
<Text size="sm" c="dimmed" ff="monospace">
{child.timestamp}
</Text>
<Badge color="dark" size="sm">
{child.method}
</Badge>
<TimestampText>+-- </TimestampText>
<TimestampText>{child.timestamp}</TimestampText>
<ChildMethodBadge>{child.method}</ChildMethodBadge>
{child.target && <Text size="sm">{child.target}</Text>}
<Badge
color={child.status === "success" ? "green" : "red"}
size="sm"
>
{child.status === "success" ? "OK" : "Error"}
<Badge color={statusColor(child.status)} size="sm">
{formatStatusLabel(child.status)}
</Badge>
<Text size="sm" c="dimmed">
{child.durationMs}ms
</Text>
<DurationText>
{formatDuration(child.durationMs)}
</DurationText>
</Group>
))}
</Stack>
@ -140,6 +177,6 @@ export function HistoryEntry({
</Stack>
</Collapse>
</Stack>
</Card>
</EntryContainer>
);
}

View file

@ -54,6 +54,15 @@ const validationIcons: Record<
error: { icon: "\u2717", color: "red" },
};
const HintText = Text.withProps({
size: "sm",
c: "dimmed",
});
function formatPackageLabel(pkg: PackageInfo): string {
return `${pkg.registryType}: ${pkg.identifier} (${pkg.runtimeHint})`;
}
export function ImportServerJsonPanel({
jsonContent,
validationResults,
@ -73,9 +82,7 @@ export function ImportServerJsonPanel({
<Stack gap="md">
<Title order={4}>Import MCP Registry server.json</Title>
<Text size="sm" c="dimmed">
Paste server.json content or drag and drop a file:
</Text>
<HintText>Paste server.json content or drag and drop a file:</HintText>
<Textarea
value={jsonContent}
@ -113,7 +120,7 @@ export function ImportServerJsonPanel({
<Radio
key={index}
value={String(index)}
label={`${pkg.registryType}: ${pkg.identifier} (${pkg.runtimeHint})`}
label={formatPackageLabel(pkg)}
/>
))}
</Stack>

View file

@ -24,6 +24,37 @@ export interface InlineElicitationRequestProps {
onCancel: () => void;
}
const RequestContainer = Paper.withProps({
p: "md",
withBorder: true,
});
const QueueLabel = Text.withProps({
size: "xs",
c: "dimmed",
});
const ItalicMessage = Text.withProps({
size: "sm",
fs: "italic",
});
const ActionsRow = Group.withProps({
justify: "flex-end",
gap: "xs",
});
const CompactButton = Button.withProps({
size: "xs",
variant: "light",
});
function getBadgeLabel(mode: "form" | "url"): string {
return mode === "form"
? "elicitation/create (form)"
: "elicitation/create (url)";
}
export function InlineElicitationRequest({
mode,
message,
@ -36,22 +67,15 @@ export function InlineElicitationRequest({
onSubmit,
onCancel,
}: InlineElicitationRequestProps) {
const badgeLabel =
mode === "form" ? "elicitation/create (form)" : "elicitation/create (url)";
return (
<Paper p="md" withBorder>
<RequestContainer>
<Stack gap="sm">
<Group justify="space-between">
<Badge color="violet">{badgeLabel}</Badge>
<Text size="xs" c="dimmed">
{queuePosition}
</Text>
<Badge color="violet">{getBadgeLabel(mode)}</Badge>
<QueueLabel>{queuePosition}</QueueLabel>
</Group>
<Text size="sm" fs="italic">
{message}
</Text>
<ItalicMessage>{message}</ItalicMessage>
{mode === "form" && schema && (
<SchemaForm
@ -73,17 +97,15 @@ export function InlineElicitationRequest({
</>
)}
<Group justify="flex-end" gap="xs">
<Button size="xs" variant="light" onClick={onCancel}>
Cancel
</Button>
<ActionsRow>
<CompactButton onClick={onCancel}>Cancel</CompactButton>
{mode === "form" && (
<Button size="xs" onClick={onSubmit}>
Submit
</Button>
)}
</Group>
</ActionsRow>
</Stack>
</Paper>
</RequestContainer>
);
}

View file

@ -19,6 +19,47 @@ export interface InlineSamplingRequestProps {
onViewDetails: () => void;
}
const RequestContainer = Paper.withProps({
p: "md",
withBorder: true,
});
const QueueLabel = Text.withProps({
size: "xs",
c: "dimmed",
});
const PreviewText = Text.withProps({
size: "sm",
c: "dimmed",
lineClamp: 2,
});
const DetailButton = Button.withProps({
variant: "subtle",
size: "xs",
});
const ActionsRow = Group.withProps({
justify: "flex-end",
gap: "xs",
});
const CompactButton = Button.withProps({
size: "xs",
variant: "light",
});
const RejectButton = Button.withProps({
size: "xs",
variant: "light",
color: "red",
});
function formatModelHints(hints: string[]): string {
return `Model hints: ${hints.join(", ")}`;
}
export function InlineSamplingRequest({
queuePosition,
modelHints,
@ -30,26 +71,18 @@ export function InlineSamplingRequest({
onViewDetails,
}: InlineSamplingRequestProps) {
return (
<Paper p="md" withBorder>
<RequestContainer>
<Stack gap="sm">
<Group justify="space-between">
<Badge color="blue">sampling/createMessage</Badge>
<Text size="xs" c="dimmed">
{queuePosition}
</Text>
<QueueLabel>{queuePosition}</QueueLabel>
</Group>
{modelHints && (
<Text size="sm">Model hints: {modelHints.join(", ")}</Text>
)}
{modelHints && <Text size="sm">{formatModelHints(modelHints)}</Text>}
<Text size="sm" c="dimmed" lineClamp={2}>
{messagePreview}
</Text>
<PreviewText>{messagePreview}</PreviewText>
<Button variant="subtle" size="xs" onClick={onViewDetails}>
View Details
</Button>
<DetailButton onClick={onViewDetails}>View Details</DetailButton>
<Textarea
size="sm"
@ -60,18 +93,12 @@ export function InlineSamplingRequest({
readOnly
/>
<Group justify="flex-end" gap="xs">
<Button size="xs" variant="light" onClick={onAutoRespond}>
Auto-respond
</Button>
<Button size="xs" variant="light" onClick={onEditAndSend}>
Edit &amp; Send
</Button>
<Button size="xs" variant="light" color="red" onClick={onReject}>
Reject
</Button>
</Group>
<ActionsRow>
<CompactButton onClick={onAutoRespond}>Auto-respond</CompactButton>
<CompactButton onClick={onEditAndSend}>Edit &amp; Send</CompactButton>
<RejectButton onClick={onReject}>Reject</RejectButton>
</ActionsRow>
</Stack>
</Paper>
</RequestContainer>
);
}

View file

@ -31,6 +31,11 @@ const LEVEL_COLORS: Record<string, { c: string; fw?: number }> = {
emergency: { c: "red", fw: 900 },
};
const ToolbarButton = Button.withProps({
variant: "light",
size: "sm",
});
export interface LogControlsProps {
currentLevel: string;
filterText: string;
@ -93,12 +98,8 @@ export function LogControls({
})}
<Group>
<Button variant="light" size="sm" onClick={onClear}>
Clear
</Button>
<Button variant="light" size="sm" onClick={onExport}>
Export
</Button>
<ToolbarButton onClick={onClear}>Clear</ToolbarButton>
<ToolbarButton onClick={onExport}>Export</ToolbarButton>
</Group>
</Stack>
);

View file

@ -6,16 +6,16 @@ export interface PendingClientRequestsProps {
children: ReactNode;
}
function formatTitle(count: number): string {
return `Pending Client Requests (${count})`;
}
export function PendingClientRequests({
count,
children,
}: PendingClientRequestsProps) {
return (
<Alert
color="blue"
variant="light"
title={`Pending Client Requests (${count})`}
>
<Alert color="blue" variant="light" title={formatTitle(count)}>
<Stack gap="md">{children}</Stack>
</Alert>
);

View file

@ -21,6 +21,15 @@ export interface PromptArgumentsFormProps {
onGetPrompt: () => void;
}
const DescriptionText = Text.withProps({
size: "sm",
c: "dimmed",
});
function formatPlaceholder(name: string): string {
return `Enter ${name}...`;
}
export function PromptArgumentsForm({
prompts,
selectedPrompt,
@ -46,9 +55,7 @@ export function PromptArgumentsForm({
placeholder="Choose a prompt..."
/>
{selectedPrompt && selectedPromptInfo?.description && (
<Text size="sm" c="dimmed">
{selectedPromptInfo.description}
</Text>
<DescriptionText>{selectedPromptInfo.description}</DescriptionText>
)}
{promptArguments.length > 0 && (
<>
@ -58,7 +65,7 @@ export function PromptArgumentsForm({
label={arg.name}
withAsterisk={arg.required}
description={arg.description}
placeholder={`Enter ${arg.name}...`}
placeholder={formatPlaceholder(arg.name)}
value={argumentValues[arg.name] || ""}
onChange={(event) =>
onArgumentChange(arg.name, event.currentTarget.value)

View file

@ -8,6 +8,11 @@ export interface PromptMessage {
audioContent?: { data: string; mimeType: string };
}
const CopyAllButton = Button.withProps({
variant: "light",
size: "sm",
});
export interface PromptMessagesDisplayProps {
messages: PromptMessage[];
onCopyAll?: () => void;
@ -22,9 +27,7 @@ export function PromptMessagesDisplay({
<Group justify="space-between">
<Title order={4}>Messages</Title>
{onCopyAll && messages.length > 0 && (
<Button variant="light" size="sm" onClick={onCopyAll}>
Copy All
</Button>
<CopyAllButton onClick={onCopyAll}>Copy All</CopyAllButton>
)}
</Group>
{messages.length === 0 ? (

View file

@ -22,8 +22,8 @@ export function ResourceListItem({
<UnstyledButton
w="100%"
p="sm"
variant="listItem"
bg={selected ? "var(--mantine-primary-color-light)" : undefined}
style={{ borderRadius: "var(--mantine-radius-md)" }}
onClick={onClick}
>
<Text fw={500}>{name}</Text>

View file

@ -1,4 +1,4 @@
import { Group, Stack, Text, Title } from "@mantine/core";
import { Flex, Group, Stack, Text, Title } from "@mantine/core";
import { AnnotationBadge } from "../../atoms/AnnotationBadge/AnnotationBadge";
import { ContentViewer } from "../../atoms/ContentViewer/ContentViewer";
import { CopyButton } from "../../atoms/CopyButton/CopyButton";
@ -27,6 +27,41 @@ function resolveContentType(mimeType: string): "json" | "image" | "text" {
return "text";
}
function formatLastUpdated(lastUpdated: string): string {
return `Last updated: ${lastUpdated}`;
}
const HeaderRow = Group.withProps({
justify: "space-between",
wrap: "nowrap",
});
const UriGroup = Group.withProps({
gap: "xs",
wrap: "nowrap",
});
const UriText = Text.withProps({
size: "sm",
c: "blue",
truncate: "end",
});
const MetaRow = Group.withProps({
justify: "space-between",
wrap: "nowrap",
});
const TimestampText = Text.withProps({
size: "xs",
c: "dimmed",
});
const MimeText = Text.withProps({
size: "sm",
c: "dimmed",
});
export function ResourcePreviewPanel({
uri,
mimeType,
@ -39,33 +74,27 @@ export function ResourcePreviewPanel({
}: ResourcePreviewPanelProps) {
return (
<Stack gap="md">
<Group justify="space-between" wrap="nowrap">
<HeaderRow>
<Title order={4}>Resource</Title>
<Group gap="xs" wrap="nowrap">
<Text size="sm" c="blue" truncate="end">
{uri}
</Text>
<UriGroup>
<UriText>{uri}</UriText>
<CopyButton value={uri} />
</Group>
</Group>
</UriGroup>
</HeaderRow>
<ContentViewer
type={resolveContentType(mimeType)}
content={content}
mimeType={mimeType}
copyable
/>
<Group justify="space-between" wrap="nowrap">
<MetaRow>
{lastUpdated ? (
<Text size="xs" c="dimmed">
Last updated: {lastUpdated}
</Text>
<TimestampText>{formatLastUpdated(lastUpdated)}</TimestampText>
) : (
<span />
<Flex />
)}
<Text size="sm" c="dimmed">
{mimeType}
</Text>
</Group>
<MimeText>{mimeType}</MimeText>
</MetaRow>
<Group justify="space-between">
<Group gap="xs">
{annotations?.audience && (

View file

@ -36,6 +36,11 @@ function parseTemplate(template: string): TemplatePart[] {
return parts;
}
const StaticPart = Text.withProps({
size: "sm",
pb: 6,
});
export function ResourceTemplateInput({
template,
variables,
@ -48,9 +53,7 @@ export function ResourceTemplateInput({
<Group gap="xs" align="flex-end">
{parts.map((part, index) =>
part.type === "static" ? (
<Text key={index} size="sm" pb={6}>
{part.value}
</Text>
<StaticPart key={index}>{part.value}</StaticPart>
) : (
<TextInput
key={part.value}

View file

@ -13,6 +13,19 @@ export interface ResultPanelProps {
onClear: () => void;
}
const ClearButton = Button.withProps({
variant: "light",
size: "sm",
});
function resolveTextType(type: string): "text" | "json" {
return type === "text" ? "text" : "json";
}
function resolveMediaType(mimeType?: string): "image" | "audio" {
return mimeType?.startsWith("image") ? "image" : "audio";
}
export function ResultPanel({ content, onClear }: ResultPanelProps) {
return (
<Stack>
@ -26,7 +39,7 @@ export function ResultPanel({ content, onClear }: ResultPanelProps) {
return (
<ContentViewer
key={index}
type={item.type === "text" ? "text" : "json"}
type={resolveTextType(item.type)}
content={item.text}
copyable
/>
@ -36,7 +49,7 @@ export function ResultPanel({ content, onClear }: ResultPanelProps) {
return (
<ContentViewer
key={index}
type={item.mimeType?.startsWith("image") ? "image" : "audio"}
type={resolveMediaType(item.mimeType)}
content={item.data}
mimeType={item.mimeType}
/>
@ -47,9 +60,7 @@ export function ResultPanel({ content, onClear }: ResultPanelProps) {
</>
)}
<Group>
<Button variant="light" size="sm" onClick={onClear}>
Clear
</Button>
<ClearButton onClick={onClear}>Clear</ClearButton>
</Group>
</Stack>
);

View file

@ -27,6 +27,21 @@ export interface RootsTableProps {
onBrowse: () => void;
}
const HintText = Text.withProps({
size: "sm",
c: "dimmed",
});
const RemoveIcon = ActionIcon.withProps({
color: "red",
variant: "subtle",
});
const AddRootButton = Button.withProps({
variant: "light",
fullWidth: true,
});
export function RootsTable({
roots,
newRootName,
@ -40,9 +55,7 @@ export function RootsTable({
return (
<Stack gap="md">
<Title order={4}>Roots Configuration</Title>
<Text size="sm" c="dimmed">
Filesystem roots exposed to the connected server:
</Text>
<HintText>Filesystem roots exposed to the connected server:</HintText>
{roots.length > 0 && (
<Table>
@ -59,13 +72,9 @@ export function RootsTable({
<Table.Td>{root.name}</Table.Td>
<Table.Td>{root.uri}</Table.Td>
<Table.Td>
<ActionIcon
color="red"
variant="subtle"
onClick={() => onRemoveRoot(root.uri)}
>
<RemoveIcon onClick={() => onRemoveRoot(root.uri)}>
X
</ActionIcon>
</RemoveIcon>
</Table.Td>
</Table.Tr>
))}
@ -73,9 +82,7 @@ export function RootsTable({
</Table>
)}
<Button variant="light" fullWidth onClick={onAddRoot}>
+ Add Root
</Button>
<AddRootButton onClick={onAddRoot}>+ Add Root</AddRootButton>
<Divider />

View file

@ -54,6 +54,39 @@ function formatPriority(value: number): string {
return `high (${value})`;
}
function formatOptional(value: unknown, fallback: string): string {
return value !== undefined ? String(value) : fallback;
}
function serializeJson(value: unknown): string {
return JSON.stringify(value);
}
const HintText = Text.withProps({
size: "sm",
c: "dimmed",
});
const PreferencesContainer = Paper.withProps({
p: "sm",
withBorder: true,
});
const ToolCard = Paper.withProps({
p: "xs",
withBorder: true,
});
const ToolName = Text.withProps({
size: "sm",
fw: 600,
});
const RejectButton = Button.withProps({
variant: "light",
color: "red",
});
export function SamplingRequestPanel({
messages,
modelHints,
@ -78,9 +111,7 @@ export function SamplingRequestPanel({
}: SamplingRequestPanelProps) {
return (
<Stack gap="md">
<Text size="sm" c="dimmed">
The server is requesting an LLM completion.
</Text>
<HintText>The server is requesting an LLM completion.</HintText>
<Title order={5}>Messages:</Title>
{messages.map((message, index) => (
@ -94,7 +125,7 @@ export function SamplingRequestPanel({
))}
{modelHints && modelHints.length > 0 && (
<Paper p="sm" withBorder>
<PreferencesContainer>
<Stack gap="xs">
<Title order={5}>Model Preferences:</Title>
<Group gap="xs">
@ -119,17 +150,19 @@ export function SamplingRequestPanel({
</Text>
)}
</Stack>
</Paper>
</PreferencesContainer>
)}
<Title order={5}>Parameters:</Title>
<Text size="sm">Max Tokens: {maxTokens ?? "not specified"}</Text>
<Text size="sm">
Stop Sequences:{" "}
{stopSequences ? JSON.stringify(stopSequences) : "not specified"}
Max Tokens: {formatOptional(maxTokens, "not specified")}
</Text>
<Text size="sm">
Temperature: {temperature !== undefined ? temperature : "not specified"}
Stop Sequences:{" "}
{stopSequences ? serializeJson(stopSequences) : "not specified"}
</Text>
<Text size="sm">
Temperature: {formatOptional(temperature, "not specified")}
</Text>
{includeContext && (
@ -143,12 +176,10 @@ export function SamplingRequestPanel({
<>
<Title order={5}>Available Tools:</Title>
{tools.map((tool) => (
<Paper key={tool.name} p="xs" withBorder>
<Text size="sm" fw={600}>
{tool.name}
</Text>
<ToolCard key={tool.name}>
<ToolName>{tool.name}</ToolName>
{tool.description && <Text size="sm">{tool.description}</Text>}
</Paper>
</ToolCard>
))}
</>
)}
@ -181,9 +212,7 @@ export function SamplingRequestPanel({
<Button variant="light" onClick={onAutoRespond}>
Auto-respond
</Button>
<Button variant="light" color="red" onClick={onReject}>
Reject
</Button>
<RejectButton onClick={onReject}>Reject</RejectButton>
<Button onClick={onSend}>Send Response</Button>
</Group>
</Stack>

View file

@ -27,6 +27,20 @@ export interface JsonSchema {
format?: string;
}
const FieldLabel = Text.withProps({
fw: 500,
size: "sm",
});
const FieldDescription = Text.withProps({
size: "xs",
c: "dimmed",
});
function serializeJson(value: unknown): string {
return JSON.stringify(value, null, 2);
}
export interface SchemaFormProps {
schema: JsonSchema;
values: Record<string, unknown>;
@ -182,14 +196,8 @@ export function SchemaForm({
if (fieldSchema.type === "object" && fieldSchema.properties) {
return (
<Stack key={fieldName} gap="sm">
<Text fw={500} size="sm">
{label}
</Text>
{description && (
<Text size="xs" c="dimmed">
{description}
</Text>
)}
<FieldLabel>{label}</FieldLabel>
{description && <FieldDescription>{description}</FieldDescription>}
<Stack gap="sm" pl="md">
<SchemaForm
schema={fieldSchema}
@ -214,7 +222,7 @@ export function SchemaForm({
disabled={disabled}
formatOnBlur
autosize
value={rawValue !== undefined ? JSON.stringify(rawValue, null, 2) : ""}
value={rawValue !== undefined ? serializeJson(rawValue) : ""}
onChange={(val) => {
try {
handleFieldChange(fieldName, JSON.parse(val));

View file

@ -39,6 +39,27 @@ const ActionsRow = Group.withProps({
gap: "xs",
});
const ServerName = Text.withProps({
fw: 600,
size: "lg",
});
const ModeText = Text.withProps({
size: "sm",
c: "dimmed",
});
const SubtleButton = Button.withProps({
variant: "subtle",
size: "xs",
});
const RemoveButton = Button.withProps({
variant: "subtle",
size: "xs",
color: "red.6",
});
export function ServerCard({
name,
version,
@ -68,9 +89,7 @@ export function ServerCard({
<Stack gap="sm">
<Group justify="space-between" wrap="wrap">
<HeaderLeft>
<Text fw={600} size="lg">
{name}
</Text>
<ServerName>{name}</ServerName>
{version && <Badge variant="outline">{version}</Badge>}
</HeaderLeft>
<HeaderRight>
@ -86,9 +105,7 @@ export function ServerCard({
<Group gap="sm">
<TransportBadge transport={transport} />
<Text size="sm" c="dimmed">
{connectionMode}
</Text>
<ModeText>{connectionMode}</ModeText>
</Group>
<ContentViewer type="text" content={command} copyable />
@ -103,29 +120,17 @@ export function ServerCard({
<Group justify="space-between">
<ActionsRow>
<Button variant="subtle" size="xs" onClick={onClone}>
Clone
</Button>
<Button variant="subtle" size="xs" onClick={onEdit}>
Edit
</Button>
<Button variant="subtle" size="xs" color="red.6" onClick={onRemove}>
Remove
</Button>
<SubtleButton onClick={onClone}>Clone</SubtleButton>
<SubtleButton onClick={onEdit}>Edit</SubtleButton>
<RemoveButton onClick={onRemove}>Remove</RemoveButton>
</ActionsRow>
<ActionsRow>
<Button variant="subtle" size="xs" onClick={onServerInfo}>
Server Info
</Button>
<Button variant="subtle" size="xs" onClick={onSettings}>
Settings
</Button>
<SubtleButton onClick={onServerInfo}>Server Info</SubtleButton>
<SubtleButton onClick={onSettings}>Settings</SubtleButton>
{canTestClientFeatures && (
<Menu>
<Menu.Target>
<Button variant="subtle" size="xs">
Test Client Features &#x25BE;
</Button>
<SubtleButton>Test Client Features &#x25BE;</SubtleButton>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item onClick={onTestSampling}>

View file

@ -26,6 +26,15 @@ export interface ServerInfoContentProps {
oauthDetails?: { authUrl?: string; scopes?: string[]; accessToken?: string };
}
const ValueText = Text.withProps({
size: "sm",
fw: 600,
});
function formatScopes(scopes: string[]): string {
return scopes.join(", ");
}
export function ServerInfoContent({
name,
version,
@ -42,19 +51,13 @@ export function ServerInfoContent({
<SimpleGrid cols={2}>
<Text size="sm">Name</Text>
<Text size="sm" fw={600}>
{name}
</Text>
<ValueText>{name}</ValueText>
<Text size="sm">Version</Text>
<Text size="sm" fw={600}>
{version}
</Text>
<ValueText>{version}</ValueText>
<Text size="sm">Protocol</Text>
<Text size="sm" fw={600}>
{protocolVersion}
</Text>
<ValueText>{protocolVersion}</ValueText>
<Text size="sm">Transport</Text>
<Badge variant="outline">{transport}</Badge>
@ -105,9 +108,7 @@ export function ServerInfoContent({
{oauthDetails.scopes && oauthDetails.scopes.length > 0 && (
<SimpleGrid cols={2}>
<Text size="sm">Scopes</Text>
<Text size="sm" fw={600}>
{oauthDetails.scopes.join(", ")}
</Text>
<ValueText>{formatScopes(oauthDetails.scopes)}</ValueText>
</SimpleGrid>
)}
{oauthDetails.accessToken && (

Some files were not shown because too many files have changed in this diff Show more