web: server-screen, footer, monitoring-sidebar, and responsive-header polish (#1683)
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

Closes #1682

Server-screen, footer, monitoring-sidebar, and responsive-header polish: connected-card highlight/scroll + Failed status, footer row + MCP-docs tooltip, Servers box, card-surface refresh (gray-0 entry cards + white code), monitoring-sidebar layout, a 1280px responsive floor honored by the header (always-icon Disconnect, always-SegmentedControl tabs, 1500px status text, always-available sidebar), a horizontally-scrollable long resource URI in the compact Protocol entry, and copy buttons beside resource URIs / network URLs.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Cliff Hall 2026-07-14 08:55:39 -04:00 • committed by GitHub
commit 8d33e76f97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 707 additions and 274 deletions

View file

@ -88,7 +88,7 @@ export interface AppsUiState {
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100dvh - var(--app-shell-header-height, 0px))",
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
gap: "md",
p: "xl",
align: "flex-start",

View file

@ -40,7 +40,7 @@ export interface ConsoleUiState {
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100dvh - var(--app-shell-header-height, 0px))",
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
gap: "md",
p: "xl",
});
@ -127,7 +127,7 @@ export function ConsoleScreen({
// See LoggingScreen: only override `h` when embedded, so the standalone
// screen keeps ScreenLayout's default full-screen height (a `h={undefined}`
// would clobber it and collapse an empty screen to its controls' height).
<ScreenLayout {...(embedded ? { h: "100%" } : {})}>
<ScreenLayout {...(embedded ? { h: "100%", pt: "md" } : {})}>
{embedded ? null : (
<Sidebar>
<SidebarCard>

View file

@ -33,7 +33,7 @@ export interface LogsUiState {
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100dvh - var(--app-shell-header-height, 0px))",
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
gap: "md",
p: "xl",
});
@ -82,7 +82,9 @@ export function LoggingScreen({
// ScreenLayout's default full-screen height. Passing `h={undefined}` here
// would clobber that default (withProps plain-spreads), collapsing an empty
// screen to its controls' height — so only override `h` when embedded.
<ScreenLayout {...(embedded ? { h: "100%" } : {})}>
// Embedded also halves the top padding (`pt: md` vs the `xl` default) so the
// panel sits closer to the sidebar's tab/search controls above it.
<ScreenLayout {...(embedded ? { h: "100%", pt: "md" } : {})}>
{embedded ? null : (
<Sidebar>
<SidebarCard>

View file

@ -34,7 +34,7 @@ export interface NetworkUiState {
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100dvh - var(--app-shell-header-height, 0px))",
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
gap: "md",
p: "xl",
});
@ -87,7 +87,7 @@ export function NetworkScreen({
// See LoggingScreen: only override `h` when embedded, so the standalone
// screen keeps ScreenLayout's default full-screen height (a `h={undefined}`
// would clobber it and collapse an empty screen to its controls' height).
<ScreenLayout {...(embedded ? { h: "100%" } : {})}>
<ScreenLayout {...(embedded ? { h: "100%", pt: "md" } : {})}>
{embedded ? null : (
<Sidebar>
<SidebarCard>

View file

@ -59,7 +59,7 @@ export interface PromptsUiState {
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100dvh - var(--app-shell-header-height, 0px))",
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
gap: "md",
p: "xl",
});
@ -108,7 +108,7 @@ const EmptyState = Text.withProps({
});
const SCROLL_MAX_HEIGHT =
"calc(100dvh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2)";
"calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px) - var(--mantine-spacing-xl) * 2)";
function hasArguments(prompt: Prompt): boolean {
return !!prompt.arguments && prompt.arguments.length > 0;

View file

@ -41,7 +41,7 @@ export interface ProtocolUiState {
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100dvh - var(--app-shell-header-height, 0px))",
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
gap: "md",
p: "xl",
});
@ -107,7 +107,7 @@ export function ProtocolScreen({
// See LoggingScreen: only override `h` when embedded, so the standalone
// screen keeps ScreenLayout's default full-screen height (a `h={undefined}`
// would clobber it and collapse an empty screen to its controls' height).
<ScreenLayout {...(embedded ? { h: "100%" } : {})}>
<ScreenLayout {...(embedded ? { h: "100%", pt: "md" } : {})}>
{embedded ? null : (
<Sidebar>
<SidebarCard>

View file

@ -74,7 +74,7 @@ export interface ResourcesUiState {
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100dvh - var(--app-shell-header-height, 0px))",
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
gap: "md",
p: "xl",
});
@ -126,7 +126,7 @@ const EmptyState = Text.withProps({
});
const SCROLL_MAX_HEIGHT =
"calc(100dvh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2)";
"calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px) - var(--mantine-spacing-xl) * 2)";
export function ResourcesScreen({
resources,

View file

@ -43,6 +43,13 @@ describe("ServerListScreen", () => {
expect(screen.getByText("Beta")).toBeInTheDocument();
});
it("renders the 'Servers' box heading", () => {
renderWithMantine(<ServerListScreen {...baseProps} />);
expect(
screen.getByRole("heading", { name: "Servers" }),
).toBeInTheDocument();
});
it("renders the empty state with no servers", () => {
renderWithMantine(<ServerListScreen {...baseProps} servers={[]} />);
expect(
@ -157,6 +164,32 @@ describe("ServerListScreen", () => {
});
});
describe("just-connected highlight (#1682)", () => {
it("draws the green highlight border on the just-connected server's card", () => {
const { container } = renderWithMantine(
<ServerListScreen
{...baseProps}
activeServer="beta"
connectedServerId="beta"
/>,
);
const highlighted = container.querySelectorAll(
'[data-variant="highlighted"]',
);
expect(highlighted).toHaveLength(1);
expect(highlighted[0]).toHaveTextContent("Beta");
});
it("draws no highlight border when connectedServerId is unset", () => {
const { container } = renderWithMantine(
<ServerListScreen {...baseProps} activeServer="beta" />,
);
expect(
container.querySelectorAll('[data-variant="highlighted"]'),
).toHaveLength(0);
});
});
describe("freshly-added highlight", () => {
it("draws a green highlight border on every highlighted server", () => {
const { container } = renderWithMantine(

View file

@ -1,10 +1,14 @@
import {
Alert,
Code,
Flex,
Group,
Paper,
ScrollArea,
SimpleGrid,
Stack,
Text,
Title,
} from "@mantine/core";
import {
DndContext,
@ -44,6 +48,12 @@ export interface ServerListScreenProps {
* draws a red border until another server is connected or attempted.
*/
erroredServerId?: string;
/**
* Id of the server that just connected successfully (#1682). Its card draws
* the green highlight border and scrolls into view — the success mirror of
* `erroredServerId`.
*/
connectedServerId?: string;
onAddManually: () => void;
onImportConfig: () => void;
onImportServerJson: () => void;
@ -69,21 +79,74 @@ export interface ServerListScreenProps {
onToggleCompact: () => void;
}
const PageContainer = Stack.withProps({
// Full-height screen wrapper (matches the Protocol/Logs screens): fills the
// viewport minus the header and footer, clips overflow, and lays the optional
// read-only banner above the box in a column.
const ScreenLayout = Flex.withProps({
variant: "screen",
direction: "column",
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
gap: "md",
p: "xl",
});
// The bordered box that fills the container (#1682 follow-up), styled like the
// "Messages" panel: a `panel`-variant Paper (display:flex column) holding the
// title + controls header and the scrolling card grid.
const PanelContainer = Paper.withProps({
withBorder: true,
p: "lg",
flex: 1,
variant: "panel",
// The box takes the same surface as the header and footer (white in light
// mode) rather than the grey app background.
bg: "var(--mantine-color-body)",
});
// Header row inside the box: "Servers" title on the left, the Export / Add /
// collapse controls on the right.
const PanelHeader = Group.withProps({
justify: "space-between",
mb: "sm",
});
// Wraps the scroll region so it fills the box below the header (`flex:1/mih:0`)
// and the inner ScrollArea can bound against it with `mah:100%`.
const CardScrollWrap = Stack.withProps({
flex: 1,
mih: 0,
gap: 0,
});
// Centered in the full-height box so the empty message sits mid-panel rather
// than clinging to the top (matches the Messages panel's empty state).
const EmptyCenter = Stack.withProps({
flex: 1,
align: "center",
justify: "center",
});
const EmptyState = Text.withProps({
c: "dimmed",
ta: "center",
py: "xl",
});
// 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.
const GRID_SURFACE_STYLES = {
root: {
"--inspector-surface-card": "var(--inspector-surface-card-server)",
},
} as const;
export function ServerListScreen({
servers,
writable = true,
activeServer,
erroredServerId,
connectedServerId,
onAddManually,
onImportConfig,
onImportServerJson,
@ -145,6 +208,7 @@ export function ServerListScreen({
onRemove,
highlighted: highlightedServerIds?.includes(server.id) ?? false,
errored: server.id === erroredServerId,
justConnected: server.id === connectedServerId,
scrollOnHighlight: server.id === firstHighlightedId,
onClearHighlight: onClearHighlight
? () => onClearHighlight(server.id)
@ -167,6 +231,7 @@ export function ServerListScreen({
cols={{ base: 1, "1040px": 2, "1560px": 3 }}
spacing="lg"
className="grid-align-start"
styles={GRID_SURFACE_STYLES}
>
{servers.map((server) =>
reorderable ? (
@ -179,7 +244,7 @@ export function ServerListScreen({
);
return (
<PageContainer>
<ScreenLayout>
{!writable && (
<Alert color="gray" variant="light" title="Read-only session">
This server list was launched with <Code>--config</Code> or an ad-hoc
@ -187,45 +252,63 @@ export function ServerListScreen({
<Code>--catalog</Code> (or no flag) to manage a writable catalog.
</Alert>
)}
<ServerListControls
serverCount={servers.length}
compact={compact}
writable={writable}
onToggleList={onToggleCompact}
onAddManually={onAddManually}
onImportConfig={onImportConfig}
onImportServerJson={onImportServerJson}
onExport={onExport}
/>
<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>
<ServerListControls
serverCount={servers.length}
compact={compact}
writable={writable}
onToggleList={onToggleCompact}
onAddManually={onAddManually}
onImportConfig={onImportConfig}
onImportServerJson={onImportServerJson}
onExport={onExport}
/>
</PanelHeader>
<ScrollArea.Autosize
mah="calc(100dvh - var(--app-shell-header-height, 60px) - var(--mantine-spacing-xl) * 2 - 60px)"
// 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
>
{servers.length === 0 ? (
<EmptyState>
No servers configured. Add a server to get started.
</EmptyState>
) : reorderable ? (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
accessibility={{ announcements }}
>
<SortableContext items={ids} strategy={rectSortingStrategy}>
{grid}
</SortableContext>
</DndContext>
<EmptyCenter>
<EmptyState>
No servers configured. Add a server to get started.
</EmptyState>
</EmptyCenter>
) : (
grid
<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
>
{reorderable ? (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
accessibility={{ announcements }}
>
<SortableContext items={ids} strategy={rectSortingStrategy}>
{grid}
</SortableContext>
</DndContext>
) : (
grid
)}
</ScrollArea.Autosize>
</CardScrollWrap>
)}
</ScrollArea.Autosize>
</PageContainer>
</PanelContainer>
</ScreenLayout>
);
}

View file

@ -29,7 +29,7 @@ export interface TasksUiState {
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100dvh - var(--app-shell-header-height, 0px))",
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
gap: "md",
p: "xl",
});
@ -59,7 +59,9 @@ export function TasksScreen({
// Embedded fills the monitoring sidebar column (100%); standalone keeps the
// ScreenLayout's default full-screen height. Only override `h` when embedded
// — passing `h={undefined}` would clobber the default (withProps spreads).
<ScreenLayout {...(embedded ? { h: "100%" } : {})}>
// Embedded also halves the top padding (`pt: md` vs `xl`) so the panel sits
// closer to the sidebar's tab/search controls (see LoggingScreen).
<ScreenLayout {...(embedded ? { h: "100%", pt: "md" } : {})}>
{embedded ? null : (
<Sidebar>
<SidebarCard>

View file

@ -61,7 +61,7 @@ export interface ToolsScreenProps {
// viewport minus the app-shell header and the screen's top+bottom xl padding,
// leaving the bottom margin the overflow used to eat.
const SCROLL_MAX_HEIGHT =
"calc(100dvh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2)";
"calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px) - var(--mantine-spacing-xl) * 2)";
// No `align` override: children stretch to the row's full height, giving each
// column pane a definite height. That definite height is what lets a column's
@ -69,7 +69,7 @@ const SCROLL_MAX_HEIGHT =
// see the Prompts/Resources preview panes this mirrors).
const ScreenLayout = Flex.withProps({
variant: "screen",
h: "calc(100dvh - var(--app-shell-header-height, 0px))",
h: "calc(100dvh - var(--app-shell-header-height, 0px) - var(--app-shell-footer-height, 0px))",
gap: "md",
p: "xl",
});