From 20e4ce7b430d476b61a6bf25e04abaa91ebca6ee Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 9 Jul 2026 20:11:43 -0400 Subject: [PATCH] fix(web): keep wide network-entry URLs from overflowing the column (#1623) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Long request URLs stretched every Network card to their own max-content width, bleeding the cards (and their status badges) past the pinned monitoring column. Mantine's ScrollArea `content` slot defaults to `min-width: max-content`, so a non-wrapping URL forced the whole list wider than its viewport. Add an opt-in `constrainContentWidth` prop to EmbeddableScrollArea that relaxes that slot to `min-width: 0`, so the list can't grow past its viewport and each row manages its own overflow — the URL already lives in a horizontal ScrollArea, which now actually scrolls. Scoped to the Network stream panel (Logs/Protocol rows wrap or truncate, so they keep the default where a long line scrolls the list). Verified in Storybook (embedded 380px column): cards clamp to the viewport, no list/page horizontal overflow, and the URL scrolls within its own area. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw --- .../EmbeddableScrollArea.test.tsx | 23 ++++++++++++ .../EmbeddableScrollArea.tsx | 19 ++++++++++ .../NetworkStreamPanel.stories.tsx | 36 +++++++++++++++++++ .../NetworkStreamPanel/NetworkStreamPanel.tsx | 6 +++- 4 files changed, 83 insertions(+), 1 deletion(-) diff --git a/clients/web/src/components/elements/EmbeddableScrollArea/EmbeddableScrollArea.test.tsx b/clients/web/src/components/elements/EmbeddableScrollArea/EmbeddableScrollArea.test.tsx index 55c78e53..a718f797 100644 --- a/clients/web/src/components/elements/EmbeddableScrollArea/EmbeddableScrollArea.test.tsx +++ b/clients/web/src/components/elements/EmbeddableScrollArea/EmbeddableScrollArea.test.tsx @@ -31,4 +31,27 @@ describe("EmbeddableScrollArea", () => { ); expect(ref.current).toBeInstanceOf(HTMLElement); }); + + it.each([false, true])( + "constrains the content width when asked (embedded=%s)", + (embedded) => { + const ref = createRef(); + renderWithMantine( + +
constrained content
+
, + ); + expect(screen.getByText("constrained content")).toBeInTheDocument(); + // The Mantine ScrollArea `content` slot has its default + // `min-width: max-content` relaxed to 0 so a long non-wrapping row + // (e.g. a network URL) can't stretch the list past its viewport (#1623). + const content = document.querySelector(".mantine-ScrollArea-content"); + expect(content).not.toBeNull(); + expect((content as HTMLElement).style.minWidth).toBe("0"); + }, + ); }); diff --git a/clients/web/src/components/elements/EmbeddableScrollArea/EmbeddableScrollArea.tsx b/clients/web/src/components/elements/EmbeddableScrollArea/EmbeddableScrollArea.tsx index 476b2db5..814bc7f7 100644 --- a/clients/web/src/components/elements/EmbeddableScrollArea/EmbeddableScrollArea.tsx +++ b/clients/web/src/components/elements/EmbeddableScrollArea/EmbeddableScrollArea.tsx @@ -17,8 +17,23 @@ export interface EmbeddableScrollAreaProps { embedded: boolean; viewportRef: Ref; children: ReactNode; + /** + * Constrain the scrolled content to the viewport width instead of letting it + * grow to its own `max-content`. Mantine's ScrollArea `content` slot defaults + * to `min-width: max-content`, so a row with non-wrapping content (e.g. a long + * network URL) stretches every card past the column and it bleeds out (#1623). + * When true, the content can shrink to the viewport and each row must manage + * its own overflow (the Network URL scrolls inside its own inner ScrollArea). + * Left off for panels whose rows already wrap/truncate (Logs, Protocol), where + * the default lets a long line scroll the list horizontally instead. + */ + constrainContentWidth?: boolean; } +// Relax the `content` slot's default `min-width: max-content` so the list can't +// grow wider than its viewport; see `constrainContentWidth`. +const CONSTRAIN_CONTENT_STYLES = { content: { minWidth: 0 } } as const; + /** * The scroll region shared by the Logs / Protocol / Network stream panels, which * render both full-size (their own tab) and embedded (the monitoring column). @@ -28,7 +43,9 @@ export function EmbeddableScrollArea({ embedded, viewportRef, children, + constrainContentWidth = false, }: EmbeddableScrollAreaProps) { + const styles = constrainContentWidth ? CONSTRAIN_CONTENT_STYLES : undefined; if (embedded) { return ( @@ -37,6 +54,7 @@ export function EmbeddableScrollArea({ mah="100%" type="scroll" offsetScrollbars + styles={styles} > {children} @@ -49,6 +67,7 @@ export function EmbeddableScrollArea({ mah={FULLSIZE_MAH} type="scroll" offsetScrollbars + styles={styles} > {children} diff --git a/clients/web/src/components/groups/NetworkStreamPanel/NetworkStreamPanel.stories.tsx b/clients/web/src/components/groups/NetworkStreamPanel/NetworkStreamPanel.stories.tsx index 56f88610..a4248d1e 100644 --- a/clients/web/src/components/groups/NetworkStreamPanel/NetworkStreamPanel.stories.tsx +++ b/clients/web/src/components/groups/NetworkStreamPanel/NetworkStreamPanel.stories.tsx @@ -57,3 +57,39 @@ export const WithEntries: Story = { export const Empty: Story = { args: { entries: [] }, }; + +const longUrlSample: FetchRequestEntry[] = [ + { + id: "n-long-1", + timestamp: new Date("2026-03-17T10:00:00Z"), + method: "GET", + url: "https://example-server.modelcontextprotocol.io/.well-known/oauth-protected-resource/mcp", + requestHeaders: {}, + responseStatus: 404, + duration: 84, + category: "auth", + }, + { + id: "n-long-2", + timestamp: new Date("2026-03-17T10:00:05Z"), + method: "GET", + url: "https://example-server.modelcontextprotocol.io/.well-known/oauth-authorization-server", + requestHeaders: {}, + responseStatus: 200, + duration: 79, + category: "auth", + }, +]; + +// Reproduces the pinned-column overflow: a long URL must scroll horizontally +// inside its own area rather than widening the card past the column (#1623). +export const EmbeddedLongUrls: Story = { + args: { entries: longUrlSample, embedded: true }, + decorators: [ + (Story) => ( +
+ +
+ ), + ], +}; diff --git a/clients/web/src/components/groups/NetworkStreamPanel/NetworkStreamPanel.tsx b/clients/web/src/components/groups/NetworkStreamPanel/NetworkStreamPanel.tsx index 509f5a98..9aa7f6e8 100644 --- a/clients/web/src/components/groups/NetworkStreamPanel/NetworkStreamPanel.tsx +++ b/clients/web/src/components/groups/NetworkStreamPanel/NetworkStreamPanel.tsx @@ -140,7 +140,11 @@ export function NetworkStreamPanel({ {!hasResults ? ( No network requests ) : ( - + {filteredEntries.map((entry) => (