fix(web): make sidebar lists, monitor panels, and Tools placeholder fill height (#1663) (#1664)

Three related "fill the available height" layout fixes to the connected screens:

- Tools & Prompts sidebars: replace the fixed magic max-height on ToolControls/
  PromptControls with the Resources pattern — sidebar Card `variant="sidebar"`
  (full-height flex column) + `Stack flex={1} mih={0}` + a flex-filling
  ScrollArea — so the list runs to the bottom of the card before scrolling.
- Logs/Console/Protocol/Network standalone screens: only override ScreenLayout
  `h` when embedded. Passing `h={undefined}` clobbered the default
  `h: calc(100dvh - header)` (withProps plain-spreads), collapsing an empty
  screen to its controls' height. Also center the Network/Protocol empty states.
- Tools placeholder: render the "Select a tool" empty state in a full-height
  `DetailCard flex={1}` (like Prompts/Resources) instead of the content-sized
  preview card, so it fills the pane.


Claude-Session: https://claude.ai/code/session_015sMW1YZXkM7fDkdPtHpE95

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Cliff Hall 2026-07-11 18:45:50 -04:00 • committed by GitHub
commit 6722438428
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 93 additions and 35 deletions

View file

@ -34,10 +34,17 @@ const PanelContainer = Paper.withProps({
variant: "panel",
});
// Centered in the full-height panel so the empty message sits mid-panel rather
// than clinging to the top of an otherwise-empty box (matches LogStreamPanel).
const EmptyCenter = Stack.withProps({
flex: 1,
align: "center",
justify: "center",
});
const EmptyState = Text.withProps({
c: "dimmed",
ta: "center",
py: "xl",
});
function formatTitle(count: number): string {
@ -133,7 +140,9 @@ export function NetworkStreamPanel({
</Group>
{!hasResults ? (
<EmptyState>No network requests</EmptyState>
<EmptyCenter>
<EmptyState>No network requests</EmptyState>
</EmptyCenter>
) : (
<EmbeddableScrollArea
embedded={embedded}

View file

@ -17,9 +17,6 @@ export interface PromptControlsProps {
onSelectPrompt: (name: string) => void;
}
const LIST_MAX_HEIGHT =
"calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2 - 160px)";
export function PromptControls({
prompts,
selectedName,
@ -39,7 +36,10 @@ export function PromptControls({
);
return (
<Stack gap="sm">
// Fill the full-height `sidebar` Card (a flex column) so the list runs to the
// bottom of the card before it scrolls, instead of being capped short by a
// fixed max-height. `mih: 0` lets the scroll child shrink and scroll.
<Stack gap="sm" flex={1} mih={0}>
<Group justify="space-between">
<Title order={4}>Prompts</Title>
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
@ -53,7 +53,7 @@ export function PromptControls({
searchText ? <ClearButton onClick={() => onSearchChange("")} /> : null
}
/>
<ScrollArea.Autosize viewportRef={viewportRef} mah={LIST_MAX_HEIGHT}>
<ScrollArea viewportRef={viewportRef} flex={1} mih={0} type="auto">
<Stack gap="xs">
{filteredPrompts.map((prompt) => (
<PromptListItem
@ -66,7 +66,7 @@ export function PromptControls({
/>
))}
</Stack>
</ScrollArea.Autosize>
</ScrollArea>
</Stack>
);
}

View file

@ -54,10 +54,17 @@ const PanelContainer = Paper.withProps({
variant: "panel",
});
// Centered in the full-height panel so the empty message sits mid-panel rather
// than clinging to the top of an otherwise-empty box (matches LogStreamPanel).
const EmptyCenter = Stack.withProps({
flex: 1,
align: "center",
justify: "center",
});
const EmptyState = Text.withProps({
c: "dimmed",
ta: "center",
py: "xl",
});
// The section header is a single "pleat" bar (rounded, with the filter-button
@ -293,7 +300,9 @@ export function ProtocolListPanel({
</Group>
{!hasResults ? (
<EmptyState>No request history</EmptyState>
<EmptyCenter>
<EmptyState>No request history</EmptyState>
</EmptyCenter>
) : (
<EmbeddableScrollArea embedded={embedded} viewportRef={viewportRef}>
<Stack gap="md">

View file

@ -17,9 +17,6 @@ export interface ToolControlsProps {
onSelectTool: (name: string) => void;
}
const LIST_MAX_HEIGHT =
"calc(100vh - var(--app-shell-header-height, 0px) - var(--mantine-spacing-xl) * 2 - 160px)";
export function ToolControls({
tools,
selectedName,
@ -40,7 +37,12 @@ export function ToolControls({
: tools;
return (
<Stack gap="sm">
// Fill the full-height `sidebar` Card (a flex column) so the scroll region
// below claims all the remaining space under the fixed title/search — the
// list runs to the bottom of the card before it scrolls, instead of being
// capped short by a fixed max-height. `mih: 0` lets the scroll child shrink
// and scroll rather than overflow the card.
<Stack gap="sm" flex={1} mih={0}>
<Group justify="space-between">
<Title order={4}>Tools</Title>
<ListChangedIndicator visible={listChanged} onRefresh={onRefreshList} />
@ -54,7 +56,7 @@ export function ToolControls({
searchText ? <ClearButton onClick={() => onSearchChange("")} /> : null
}
/>
<ScrollArea.Autosize viewportRef={viewportRef} mah={LIST_MAX_HEIGHT}>
<ScrollArea viewportRef={viewportRef} flex={1} mih={0} type="auto">
<Stack gap="xs">
{filteredTools.map((tool) => (
<ToolListItem
@ -67,7 +69,7 @@ export function ToolControls({
/>
))}
</Stack>
</ScrollArea.Autosize>
</ScrollArea>
</Stack>
);
}