From 71d33d70c4e7e7fc2ee90fe50452014e7dde4c24 Mon Sep 17 00:00:00 2001 From: cliffhall Date: Thu, 9 Jul 2026 15:05:34 -0400 Subject: [PATCH] feat(web): scroll the failed server card into view on connection failure (#1621) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Opening the monitoring column on a failed connection reflows the server grid to fewer/narrower columns, which can push the failed card below the fold. Scroll it into view so the red-bordered card is visible without hunting for it. - ServerCard scrolls itself into view on the disconnected→errored transition (mirrors the freshly-added highlight scroll), deferred ~320ms so the column open + grid reflow settle first — otherwise scrollIntoView targets the card's pre-reflow position. Guarded by a ref so it fires once per failure and won't fight a user who scrolls away. Verified live: the failed `time` card scrolls fully into view after the column opens. Full `npm run ci` passes. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01BrihGWcrM9JGRyu41nzZYw --- .../groups/ServerCard/ServerCard.test.tsx | 56 +++++++++++++++++++ .../groups/ServerCard/ServerCard.tsx | 23 ++++++++ 2 files changed, 79 insertions(+) diff --git a/clients/web/src/components/groups/ServerCard/ServerCard.test.tsx b/clients/web/src/components/groups/ServerCard/ServerCard.test.tsx index 29b7bf91..0ad12edc 100644 --- a/clients/web/src/components/groups/ServerCard/ServerCard.test.tsx +++ b/clients/web/src/components/groups/ServerCard/ServerCard.test.tsx @@ -414,5 +414,61 @@ describe("ServerCard", () => { container.querySelector('[data-variant="highlighted"]'), ).toBeNull(); }); + + it("scrolls the card into view on the errored transition", () => { + vi.useFakeTimers(); + const scrollIntoView = vi.fn(); + const orig = Element.prototype.scrollIntoView; + Element.prototype.scrollIntoView = scrollIntoView; + try { + const { rerender } = renderWithMantine( + , + ); + // No scroll while not errored. + vi.advanceTimersByTime(1000); + expect(scrollIntoView).not.toHaveBeenCalled(); + + // Becoming errored schedules a deferred scroll (past the column open). + rerender( + , + ); + expect(scrollIntoView).not.toHaveBeenCalled(); + vi.advanceTimersByTime(320); + expect(scrollIntoView).toHaveBeenCalledTimes(1); + + // A further re-render while still errored does not scroll again (so it + // won't fight a user who scrolled away). + rerender( + , + ); + vi.advanceTimersByTime(1000); + expect(scrollIntoView).toHaveBeenCalledTimes(1); + } finally { + Element.prototype.scrollIntoView = orig; + vi.useRealTimers(); + } + }); + + it("does not scroll when mounted already errored (no transition)", () => { + vi.useFakeTimers(); + const scrollIntoView = vi.fn(); + const orig = Element.prototype.scrollIntoView; + Element.prototype.scrollIntoView = scrollIntoView; + try { + renderWithMantine( + , + ); + vi.advanceTimersByTime(1000); + expect(scrollIntoView).not.toHaveBeenCalled(); + } finally { + Element.prototype.scrollIntoView = orig; + vi.useRealTimers(); + } + }); }); }); diff --git a/clients/web/src/components/groups/ServerCard/ServerCard.tsx b/clients/web/src/components/groups/ServerCard/ServerCard.tsx index 6d2bfaa0..54fd2e78 100644 --- a/clients/web/src/components/groups/ServerCard/ServerCard.tsx +++ b/clients/web/src/components/groups/ServerCard/ServerCard.tsx @@ -101,6 +101,14 @@ const RemoveButton = Button.withProps({ color: "red.6", }); +// Delay before scrolling a newly-failed card into view (#1621). A connection +// failure both flags the card and opens the monitoring column, which reflows +// the server grid (fewer, narrower columns) and can push the failed card below +// the fold. Deferring past the column's open lets that reflow settle so +// `scrollIntoView` targets the card's final position rather than its pre-reflow +// one. Kept just above the column's slide duration. +const ERROR_SCROLL_DELAY_MS = 320; + function getTransport(config: MCPServerConfig): ServerType { return config.type ?? "stdio"; } @@ -155,6 +163,21 @@ export function ServerCard({ rootRef.current?.scrollIntoView({ behavior: "smooth", block: "center" }); } }, [highlighted, scrollOnHighlight]); + + // Scroll the failed card into view on the disconnected→errored transition + // (#1621), deferred so the monitoring column's open + grid reflow settle + // first. Guarded by a ref so a re-render while still errored doesn't re-scroll + // and fight the user if they've scrolled away. + const wasErroredRef = useRef(errored); + useEffect(() => { + const justErrored = errored && !wasErroredRef.current; + wasErroredRef.current = errored; + if (!justErrored) return; + const timer = setTimeout(() => { + rootRef.current?.scrollIntoView({ behavior: "smooth", block: "center" }); + }, ERROR_SCROLL_DELAY_MS); + return () => clearTimeout(timer); + }, [errored]); const transport = getTransport(config); const commandOrUrl = getCommandOrUrl(config); const version = info?.version;