refactor(web): sweep AGENTS.md TypeScript + Mantine rule violations (#1771)
Codebase-wide sweep bringing the web client into strict compliance with the AGENTS.md TypeScript and Mantine/React conventions: ~170 inline 2+-static-prop elements extracted to named .withProps() constants across ~77 files, the AGENTS.md rule wording tightened to match (static-prop boundary + Accordion/Transition/data-*/Box exemptions), a real ScreenStage Box->Flex layout regression fixed (direction: column) with a Storybook regression guard, and an App.tsx double-cast tightened. Closes #1762.
This commit is contained in:
parent
8aba3122c5
commit
da37692485
6 changed files with 1567 additions and 991 deletions
|
|
@ -107,10 +107,13 @@ const SidebarCard = Card.withProps({
|
|||
// `variant="preview"` (overflow: hidden) keeps the full-height card from
|
||||
// bleeding past the viewport: the running app's iframe fills it, and the
|
||||
// app-input form scrolls internally (see AppDetailPanel's PanelScroll).
|
||||
// `flex: 1` + `h: "100%"` make it fill the screen column (both call sites do).
|
||||
const ContentCard = Card.withProps({
|
||||
withBorder: true,
|
||||
padding: "lg",
|
||||
variant: "preview",
|
||||
flex: 1,
|
||||
h: "100%",
|
||||
});
|
||||
|
||||
const EmptyState = Text.withProps({
|
||||
|
|
@ -152,6 +155,17 @@ const HeaderActions = Group.withProps({
|
|||
wrap: "nowrap",
|
||||
});
|
||||
|
||||
const BackToInputButton = Button.withProps({
|
||||
variant: "subtle",
|
||||
size: "sm",
|
||||
leftSection: <MdArrowBack aria-hidden size={16} />,
|
||||
});
|
||||
|
||||
const CloseIconButton = ActionIcon.withProps({
|
||||
variant: "subtle",
|
||||
"aria-label": "Close",
|
||||
});
|
||||
|
||||
// The host-controlled box the running app sits within. Its size is driven by
|
||||
// the host's layout (window resize, sidebar toggle, maximize) and NOT by the
|
||||
// view's reported content height — that drives the inner RendererFrame — so the
|
||||
|
|
@ -552,7 +566,7 @@ export function AppsScreen({
|
|||
if (!sandboxPath) {
|
||||
return (
|
||||
<ScreenLayout>
|
||||
<ContentCard flex={1} h="100%">
|
||||
<ContentCard>
|
||||
<EmptyState>
|
||||
MCP Apps are unavailable — the sandbox could not be reached.
|
||||
</EmptyState>
|
||||
|
|
@ -589,8 +603,6 @@ export function AppsScreen({
|
|||
)}
|
||||
|
||||
<ContentCard
|
||||
flex={1}
|
||||
h="100%"
|
||||
data-testid="apps-form"
|
||||
data-app-status={running ? appStatus : "idle"}
|
||||
data-app-error={running ? appError?.message : undefined}
|
||||
|
|
@ -608,14 +620,9 @@ export function AppsScreen({
|
|||
</HeaderLabel>
|
||||
<HeaderActions>
|
||||
{running && selectedHasFields && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
leftSection={<MdArrowBack aria-hidden size={16} />}
|
||||
onClick={handleBackToInput}
|
||||
>
|
||||
<BackToInputButton onClick={handleBackToInput}>
|
||||
Back to Input
|
||||
</Button>
|
||||
</BackToInputButton>
|
||||
)}
|
||||
{running && (
|
||||
<Tooltip label={maximized ? "Restore" : "Maximize"}>
|
||||
|
|
@ -633,13 +640,9 @@ export function AppsScreen({
|
|||
</Tooltip>
|
||||
)}
|
||||
<Tooltip label="Close">
|
||||
<ActionIcon
|
||||
variant="subtle"
|
||||
onClick={handleClose}
|
||||
aria-label="Close"
|
||||
>
|
||||
<CloseIconButton onClick={handleClose}>
|
||||
<MdClose aria-hidden size={20} />
|
||||
</ActionIcon>
|
||||
</CloseIconButton>
|
||||
</Tooltip>
|
||||
</HeaderActions>
|
||||
</HeaderRow>
|
||||
|
|
|
|||
|
|
@ -62,6 +62,20 @@ const PanelContainer = Paper.withProps({
|
|||
variant: "panel",
|
||||
});
|
||||
|
||||
// Header row inside the panel: "Server Console" title beside the sort/clear/
|
||||
// export controls.
|
||||
const PanelHeaderRow = Group.withProps({
|
||||
justify: "space-between",
|
||||
mb: "sm",
|
||||
});
|
||||
|
||||
// Sidebar filter field; `rightSectionPointerEvents="auto"` lets the clear button
|
||||
// in the right section stay clickable.
|
||||
const SearchInput = TextInput.withProps({
|
||||
placeholder: "Search...",
|
||||
rightSectionPointerEvents: "auto",
|
||||
});
|
||||
|
||||
const EmptyCenter = Stack.withProps({
|
||||
flex: 1,
|
||||
align: "center",
|
||||
|
|
@ -133,13 +147,11 @@ export function ConsoleScreen({
|
|||
<SidebarCard>
|
||||
<Stack gap="md">
|
||||
<Title order={4}>Console</Title>
|
||||
<TextInput
|
||||
placeholder="Search..."
|
||||
<SearchInput
|
||||
value={filterText}
|
||||
onChange={(e) =>
|
||||
onUiChange({ filterText: e.currentTarget.value })
|
||||
}
|
||||
rightSectionPointerEvents="auto"
|
||||
rightSection={
|
||||
filterText ? (
|
||||
<ClearButton
|
||||
|
|
@ -153,7 +165,7 @@ export function ConsoleScreen({
|
|||
</Sidebar>
|
||||
)}
|
||||
<PanelContainer>
|
||||
<Group justify="space-between" mb="sm">
|
||||
<PanelHeaderRow>
|
||||
<Title order={4}>Server Console</Title>
|
||||
<Group>
|
||||
<SortToggle
|
||||
|
|
@ -176,7 +188,7 @@ export function ConsoleScreen({
|
|||
Export
|
||||
</Button>
|
||||
</Group>
|
||||
</Group>
|
||||
</PanelHeaderRow>
|
||||
{filteredEntries.length > 0 ? (
|
||||
<EmbeddableScrollArea embedded={embedded} viewportRef={viewportRef}>
|
||||
<Stack gap="xs">
|
||||
|
|
|
|||
|
|
@ -109,6 +109,18 @@ const EmptyState = Text.withProps({
|
|||
py: "xl",
|
||||
});
|
||||
|
||||
// Centered loader/status column shown while a prompt is being fetched.
|
||||
const CenteredStatus = Stack.withProps({
|
||||
align: "center",
|
||||
py: "xl",
|
||||
});
|
||||
|
||||
const PromptErrorAlert = Alert.withProps({
|
||||
color: "red",
|
||||
variant: "light",
|
||||
title: "Prompt Error",
|
||||
});
|
||||
|
||||
const SCROLL_MAX_HEIGHT =
|
||||
"calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px) - var(--mantine-spacing-xl) * 2)";
|
||||
|
||||
|
|
@ -212,10 +224,10 @@ export function PromptsScreen({
|
|||
onClick={handleClosePreview}
|
||||
/>
|
||||
</Group>
|
||||
<Stack align="center" py="xl">
|
||||
<CenteredStatus>
|
||||
<Loader size="sm" />
|
||||
<Text c="dimmed">Loading prompt...</Text>
|
||||
</Stack>
|
||||
</CenteredStatus>
|
||||
</Stack>
|
||||
</PreviewCard>
|
||||
);
|
||||
|
|
@ -230,9 +242,9 @@ export function PromptsScreen({
|
|||
onClick={handleClosePreview}
|
||||
/>
|
||||
</Group>
|
||||
<Alert color="red" variant="light" title="Prompt Error">
|
||||
<PromptErrorAlert>
|
||||
{getPromptState.error ?? "Failed to get prompt"}
|
||||
</Alert>
|
||||
</PromptErrorAlert>
|
||||
</Stack>
|
||||
</PreviewCard>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -142,6 +142,18 @@ const EmptyState = Text.withProps({
|
|||
py: "xl",
|
||||
});
|
||||
|
||||
// Centered loader/status column shown while a resource is being read.
|
||||
const CenteredStatus = Stack.withProps({
|
||||
align: "center",
|
||||
py: "xl",
|
||||
});
|
||||
|
||||
const ReadErrorAlert = Alert.withProps({
|
||||
color: "red",
|
||||
variant: "light",
|
||||
title: "Read Error",
|
||||
});
|
||||
|
||||
const SCROLL_MAX_HEIGHT =
|
||||
"calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px) - var(--mantine-spacing-xl) * 2)";
|
||||
|
||||
|
|
@ -248,10 +260,10 @@ export function ResourcesScreen({
|
|||
onClick={handleClosePreview}
|
||||
/>
|
||||
</Group>
|
||||
<Stack align="center" py="xl">
|
||||
<CenteredStatus>
|
||||
<Loader size="sm" />
|
||||
<Text c="dimmed">Reading resource...</Text>
|
||||
</Stack>
|
||||
</CenteredStatus>
|
||||
</Stack>
|
||||
</PreviewCard>
|
||||
);
|
||||
|
|
@ -267,9 +279,9 @@ export function ResourcesScreen({
|
|||
onClick={handleClosePreview}
|
||||
/>
|
||||
</Group>
|
||||
<Alert color="red" variant="light" title="Read Error">
|
||||
<ReadErrorAlert>
|
||||
{readState.error ?? "Failed to read resource"}
|
||||
</Alert>
|
||||
</ReadErrorAlert>
|
||||
</Stack>
|
||||
</PreviewCard>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -134,13 +134,57 @@ const EmptyState = Text.withProps({
|
|||
// Override the card-surface var for the whole grid so every server card (in any
|
||||
// state — the theme's default/highlighted/errored/disabled Card variants all
|
||||
// read `--inspector-surface-card`) picks up the faintly-grey server tone,
|
||||
// without touching the shared token or the variant logic.
|
||||
// without touching the shared token or the variant logic. `alignItems: start`
|
||||
// keeps rows top-aligned so a taller card (e.g. one whose action row wrapped)
|
||||
// doesn't stretch its shorter row-mates.
|
||||
const GRID_SURFACE_STYLES = {
|
||||
root: {
|
||||
"--inspector-surface-card": "var(--inspector-surface-card-server)",
|
||||
alignItems: "start",
|
||||
},
|
||||
} as const;
|
||||
|
||||
// Read-only banner shown above the panel when the server list isn't writable.
|
||||
const ReadOnlyAlert = Alert.withProps({
|
||||
color: "gray",
|
||||
variant: "light",
|
||||
title: "Read-only session",
|
||||
});
|
||||
|
||||
// Semantic h3 (visually h4) so it doesn't skip a level under the disconnected
|
||||
// header's h2 "MCP Inspector" (heading-order a11y). The other panels render h4
|
||||
// directly because they only show while connected, when the header carries no
|
||||
// heading to skip under.
|
||||
const PanelTitle = Title.withProps({
|
||||
order: 3,
|
||||
size: "h4",
|
||||
});
|
||||
|
||||
// The server card grid. Container queries (not viewport) so column count tracks
|
||||
// the actual space the grid occupies. The 2- and 3-column thresholds (1040px /
|
||||
// 1560px) keep each card ≥ ~505px wide at the switch point: container =
|
||||
// N·card + (N−1)·gap with gap = lg spacing (20px), i.e. 1040 = 2·510+20 and
|
||||
// 1560 = 3·507+40. Below ~500px a connected card's action row (Clone/Edit/Remove
|
||||
// + Connection Info/Settings, ~440px with padding) wraps and stacks, making that
|
||||
// card taller than its neighbours; dropping to fewer, wider columns instead keeps
|
||||
// every card the same height. (#1528)
|
||||
const ServerGrid = SimpleGrid.withProps({
|
||||
type: "container",
|
||||
cols: { base: 1, "1040px": 2, "1560px": 3 },
|
||||
spacing: "lg",
|
||||
styles: GRID_SURFACE_STYLES,
|
||||
});
|
||||
|
||||
// Same scrollbar treatment as the Protocol/Network/Logging list panels (#1474):
|
||||
// reserve a gutter so the bar never overlays the right edge of the server cards
|
||||
// (occluding their action icons / status badges), and only show it while
|
||||
// actively scrolling rather than popping in on hover.
|
||||
const CardScrollArea = ScrollArea.Autosize.withProps({
|
||||
mah: "100%",
|
||||
type: "scroll",
|
||||
offsetScrollbars: true,
|
||||
});
|
||||
|
||||
export function ServerListScreen({
|
||||
servers,
|
||||
writable = true,
|
||||
|
|
@ -217,22 +261,7 @@ export function ServerListScreen({
|
|||
});
|
||||
|
||||
const grid = (
|
||||
<SimpleGrid
|
||||
// Container queries (not viewport) so column count tracks the actual
|
||||
// space the grid occupies. The 2- and 3-column thresholds (1040px /
|
||||
// 1560px) keep each card ≥ ~505px wide at the switch point: container =
|
||||
// N·card + (N−1)·gap with gap = lg spacing (20px), i.e. 1040 = 2·510+20
|
||||
// and 1560 = 3·507+40. Below ~500px a connected card's action row
|
||||
// (Clone/Edit/Remove + Connection Info/Settings, ~440px with padding)
|
||||
// wraps and stacks, making that card taller than its neighbours; dropping
|
||||
// to fewer, wider columns instead keeps every card the same height.
|
||||
// (#1528)
|
||||
type="container"
|
||||
cols={{ base: 1, "1040px": 2, "1560px": 3 }}
|
||||
spacing="lg"
|
||||
className="grid-align-start"
|
||||
styles={GRID_SURFACE_STYLES}
|
||||
>
|
||||
<ServerGrid>
|
||||
{servers.map((server) =>
|
||||
reorderable ? (
|
||||
<SortableServerCard key={server.id} {...cardProps(server)} />
|
||||
|
|
@ -240,27 +269,21 @@ export function ServerListScreen({
|
|||
<ServerCard key={server.id} {...cardProps(server)} />
|
||||
),
|
||||
)}
|
||||
</SimpleGrid>
|
||||
</ServerGrid>
|
||||
);
|
||||
|
||||
return (
|
||||
<ScreenLayout>
|
||||
{!writable && (
|
||||
<Alert color="gray" variant="light" title="Read-only session">
|
||||
<ReadOnlyAlert>
|
||||
This server list was launched with <Code>--config</Code> or an ad-hoc
|
||||
server and can't be edited here. Changes won't be saved. Use{" "}
|
||||
<Code>--catalog</Code> (or no flag) to manage a writable catalog.
|
||||
</Alert>
|
||||
</ReadOnlyAlert>
|
||||
)}
|
||||
<PanelContainer>
|
||||
<PanelHeader>
|
||||
{/* Semantic h3 (visually h4) so it doesn't skip a level under the
|
||||
disconnected header's h2 "MCP Inspector" (heading-order a11y). The
|
||||
other panels render h4 directly because they only show while
|
||||
connected, when the header carries no heading to skip under. */}
|
||||
<Title order={3} size="h4">
|
||||
Servers
|
||||
</Title>
|
||||
<PanelTitle>Servers</PanelTitle>
|
||||
<ServerListControls
|
||||
serverCount={servers.length}
|
||||
compact={compact}
|
||||
|
|
@ -281,16 +304,7 @@ export function ServerListScreen({
|
|||
</EmptyCenter>
|
||||
) : (
|
||||
<CardScrollWrap>
|
||||
<ScrollArea.Autosize
|
||||
mah="100%"
|
||||
// Same scrollbar treatment as the Protocol/Network/Logging list
|
||||
// panels (#1474): reserve a gutter so the bar never overlays the
|
||||
// right edge of the server cards (occluding their action icons /
|
||||
// status badges), and only show it while actively scrolling
|
||||
// rather than popping in on hover.
|
||||
type="scroll"
|
||||
offsetScrollbars
|
||||
>
|
||||
<CardScrollArea>
|
||||
{reorderable ? (
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
|
|
@ -305,7 +319,7 @@ export function ServerListScreen({
|
|||
) : (
|
||||
grid
|
||||
)}
|
||||
</ScrollArea.Autosize>
|
||||
</CardScrollArea>
|
||||
</CardScrollWrap>
|
||||
)}
|
||||
</PanelContainer>
|
||||
|
|
|
|||
|
|
@ -164,6 +164,23 @@ export function ToolsScreen({
|
|||
: undefined;
|
||||
const isExecuting = callState?.status === "pending";
|
||||
|
||||
const handleSelectTool = (name: string) => {
|
||||
// Seed the form with the tool's schema defaults so default-only fields the
|
||||
// user never edits are still sent on execute (the form shows defaults via
|
||||
// resolveValue, but onChange only writes edited fields).
|
||||
const tool = tools.find((t) => t.name === name);
|
||||
// `name` always comes from the rendered tools list (ToolControls only emits
|
||||
// names it was given), so the lookup never misses; the empty-object fallback
|
||||
// is an unreachable defensive default.
|
||||
let nextFormValues: Record<string, unknown> = {};
|
||||
/* v8 ignore next -- unreachable: onSelectTool always names a tool in the list */
|
||||
if (tool)
|
||||
nextFormValues = collectSchemaDefaults(
|
||||
toFormSchema(tool.inputSchema) ?? {},
|
||||
);
|
||||
onUiChange({ ...ui, selectedToolName: name, formValues: nextFormValues });
|
||||
};
|
||||
|
||||
return (
|
||||
<ScreenLayout>
|
||||
<Sidebar>
|
||||
|
|
@ -177,27 +194,7 @@ export function ToolsScreen({
|
|||
onRefreshList={onRefreshList}
|
||||
pagination={pagination}
|
||||
onSearchChange={(value) => onUiChange({ ...ui, search: value })}
|
||||
onSelectTool={(name) => {
|
||||
// Seed the form with the tool's schema defaults so default-only
|
||||
// fields the user never edits are still sent on execute (the
|
||||
// form shows defaults via resolveValue, but onChange only writes
|
||||
// edited fields).
|
||||
const tool = tools.find((t) => t.name === name);
|
||||
// `name` always comes from the rendered tools list (ToolControls
|
||||
// only emits names it was given), so the lookup never misses; the
|
||||
// empty-object fallback is an unreachable defensive default.
|
||||
let formValues: Record<string, unknown> = {};
|
||||
/* v8 ignore next -- unreachable: onSelectTool always names a tool in the list */
|
||||
if (tool)
|
||||
formValues = collectSchemaDefaults(
|
||||
toFormSchema(tool.inputSchema) ?? {},
|
||||
);
|
||||
onUiChange({
|
||||
...ui,
|
||||
selectedToolName: name,
|
||||
formValues,
|
||||
});
|
||||
}}
|
||||
onSelectTool={handleSelectTool}
|
||||
/>
|
||||
</SidebarCard>
|
||||
</Sidebar>
|
||||
|
|
|
|||
Loading…
Reference in a new issue