From e8fb4188203255d9531b37259cae22c1f494eef0 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 31 Mar 2026 18:42:36 +0100 Subject: [PATCH] fix(site): delay desktop VNC connection until tab is selected (#23861) --- .../tools/InlineDesktopPreview.tsx | 1 + .../components/RightPanel/DesktopPanel.tsx | 18 ++++- .../components/Sidebar/SidebarTabView.tsx | 7 +- .../hooks/useDesktopConnection.test.ts | 78 +++++++++++-------- .../AgentsPage/hooks/useDesktopConnection.ts | 9 ++- 5 files changed, 74 insertions(+), 39 deletions(-) diff --git a/site/src/pages/AgentsPage/components/ChatElements/tools/InlineDesktopPreview.tsx b/site/src/pages/AgentsPage/components/ChatElements/tools/InlineDesktopPreview.tsx index 775e13dc84..13395d5473 100644 --- a/site/src/pages/AgentsPage/components/ChatElements/tools/InlineDesktopPreview.tsx +++ b/site/src/pages/AgentsPage/components/ChatElements/tools/InlineDesktopPreview.tsx @@ -36,6 +36,7 @@ export const InlineDesktopPreview: React.FC<{ // real hook skips its WebSocket connection logic entirely. const realConnection = useDesktopConnection({ chatId: connectionOverride ? undefined : chatId, + activated: true, }); const { status, attach } = connectionOverride ?? realConnection; const [aspectRatio, setAspectRatio] = useState(DEFAULT_ASPECT); diff --git a/site/src/pages/AgentsPage/components/RightPanel/DesktopPanel.tsx b/site/src/pages/AgentsPage/components/RightPanel/DesktopPanel.tsx index 4937f5fe0e..bbd8549380 100644 --- a/site/src/pages/AgentsPage/components/RightPanel/DesktopPanel.tsx +++ b/site/src/pages/AgentsPage/components/RightPanel/DesktopPanel.tsx @@ -1,4 +1,5 @@ import type { FC } from "react"; +import { useState } from "react"; import { Button } from "#/components/Button/Button"; import { Spinner } from "#/components/Spinner/Spinner"; import { useDesktopConnection } from "../../hooks/useDesktopConnection"; @@ -12,6 +13,8 @@ type DesktopConnectionStatus = interface DesktopPanelProps { chatId: string; + /** When true the panel is the active sidebar tab. */ + isVisible?: boolean; } export interface DesktopPanelViewProps { @@ -20,8 +23,19 @@ export interface DesktopPanelViewProps { attach: (container: HTMLElement) => void; } -export const DesktopPanel: FC = ({ chatId }) => { - const { status, reconnect, attach } = useDesktopConnection({ chatId }); +export const DesktopPanel: FC = ({ chatId, isVisible }) => { + // Delay the VNC connection until the desktop tab is first selected. + // Once activated, the connection stays alive even when the tab is + // switched away — mirrors the terminal panel pattern from PR #23231. + const [activated, setActivated] = useState(false); + if (isVisible && !activated) { + setActivated(true); + } + + const { status, reconnect, attach } = useDesktopConnection({ + chatId, + activated, + }); return ( ); diff --git a/site/src/pages/AgentsPage/components/Sidebar/SidebarTabView.tsx b/site/src/pages/AgentsPage/components/Sidebar/SidebarTabView.tsx index bef029e365..3341f41850 100644 --- a/site/src/pages/AgentsPage/components/Sidebar/SidebarTabView.tsx +++ b/site/src/pages/AgentsPage/components/Sidebar/SidebarTabView.tsx @@ -142,7 +142,12 @@ export const SidebarTabView: FC = ({ if (desktopChatId) { allPanels.push({ id: "desktop", - content: , + content: ( + + ), }); } diff --git a/site/src/pages/AgentsPage/hooks/useDesktopConnection.test.ts b/site/src/pages/AgentsPage/hooks/useDesktopConnection.test.ts index 09a85d5aa2..34cfd36eaa 100644 --- a/site/src/pages/AgentsPage/hooks/useDesktopConnection.test.ts +++ b/site/src/pages/AgentsPage/hooks/useDesktopConnection.test.ts @@ -160,7 +160,7 @@ describe("useDesktopConnection", () => { it("does nothing when chatId is undefined", () => { const { result } = renderHook(() => - useDesktopConnection({ chatId: undefined }), + useDesktopConnection({ chatId: undefined, activated: true }), ); expect(result.current.status).toBe("idle"); @@ -171,7 +171,7 @@ describe("useDesktopConnection", () => { it("auto-connects on mount when chatId is set", () => { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); expect(mockWatchChatDesktop).toHaveBeenCalledWith("chat-1"); @@ -185,7 +185,9 @@ describe("useDesktopConnection", () => { }); it("sets scaleViewport and resizeSession on the RFB instance", () => { - renderHook(() => useDesktopConnection({ chatId: "chat-1" })); + renderHook(() => + useDesktopConnection({ chatId: "chat-1", activated: true }), + ); const rfb = getLastRFBInstance(); expect(rfb.scaleViewport).toBe(true); @@ -194,7 +196,7 @@ describe("useDesktopConnection", () => { it("transitions to error on securityfailure", () => { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); const rfb = getLastRFBInstance(); @@ -213,7 +215,7 @@ describe("useDesktopConnection", () => { try { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); const rfb1 = getLastRFBInstance(); @@ -263,7 +265,7 @@ describe("useDesktopConnection", () => { try { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); const rfb1 = getLastRFBInstance(); @@ -298,7 +300,9 @@ describe("useDesktopConnection", () => { vi.useFakeTimers(); try { - renderHook(() => useDesktopConnection({ chatId: "chat-1" })); + renderHook(() => + useDesktopConnection({ chatId: "chat-1", activated: true }), + ); let rfb = getLastRFBInstance(); act(() => rfb.simulateEvent("connect")); @@ -330,7 +334,7 @@ describe("useDesktopConnection", () => { try { const { unmount } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); const rfb = getLastRFBInstance(); @@ -351,7 +355,7 @@ describe("useDesktopConnection", () => { it("tears down and reconnects when chatId changes", () => { const { result, rerender } = renderHook( ({ chatId }: { chatId: string | undefined }) => - useDesktopConnection({ chatId }), + useDesktopConnection({ chatId, activated: true }), { initialProps: { chatId: "chat-aaa" as string | undefined } }, ); @@ -374,7 +378,7 @@ describe("useDesktopConnection", () => { it("attach() appends the offscreen container to the target", () => { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); const rfb = getLastRFBInstance(); @@ -389,7 +393,7 @@ describe("useDesktopConnection", () => { it("attach() moves the canvas between containers without reconnecting", () => { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); const rfb = getLastRFBInstance(); @@ -413,7 +417,7 @@ describe("useDesktopConnection", () => { it("stores remote clipboard text when the server sends it", async () => { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); const rfb = getLastRFBInstance(); @@ -431,7 +435,7 @@ describe("useDesktopConnection", () => { try { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); const rfb = getLastRFBInstance(); @@ -455,7 +459,7 @@ describe("useDesktopConnection", () => { try { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); const rfb1 = getLastRFBInstance(); @@ -485,7 +489,7 @@ describe("useDesktopConnection", () => { it("attach() is a no-op when the screen is already in the container", () => { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); const rfb = getLastRFBInstance(); @@ -504,7 +508,7 @@ describe("useDesktopConnection", () => { try { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); let rfb = getLastRFBInstance(); @@ -535,7 +539,7 @@ describe("useDesktopConnection", () => { try { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); let rfb = getLastRFBInstance(); @@ -570,7 +574,7 @@ describe("useDesktopConnection", () => { try { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); let rfb = getLastRFBInstance(); @@ -602,7 +606,7 @@ describe("useDesktopConnection", () => { try { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); let rfb = getLastRFBInstance(); @@ -633,7 +637,7 @@ describe("useDesktopConnection", () => { try { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); const rfb1 = getLastRFBInstance(); @@ -669,7 +673,7 @@ describe("useDesktopConnection", () => { try { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); const rfb1 = getLastRFBInstance(); @@ -698,7 +702,7 @@ describe("useDesktopConnection", () => { try { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); expect(result.current.status).toBe("connecting"); @@ -718,7 +722,7 @@ describe("useDesktopConnection", () => { try { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); const rfb = getLastRFBInstance(); @@ -744,7 +748,7 @@ describe("useDesktopConnection", () => { try { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); const rfb = getLastRFBInstance(); @@ -769,7 +773,7 @@ describe("useDesktopConnection", () => { try { const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); let rfb = getLastRFBInstance(); @@ -804,7 +808,7 @@ describe("useDesktopConnection", () => { it("resets to idle when chatId becomes undefined", () => { const { result, rerender } = renderHook( ({ chatId }: { chatId: string | undefined }) => - useDesktopConnection({ chatId }), + useDesktopConnection({ chatId, activated: true }), { initialProps: { chatId: "chat-1" as string | undefined } }, ); @@ -825,7 +829,7 @@ describe("useDesktopConnection", () => { try { const { result, rerender } = renderHook( ({ chatId }: { chatId: string | undefined }) => - useDesktopConnection({ chatId }), + useDesktopConnection({ chatId, activated: true }), { initialProps: { chatId: "chat-aaa" as string | undefined } }, ); @@ -861,7 +865,7 @@ describe("useDesktopConnection", () => { FakeRFB.throwOnConstruct = true; const { result } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); expect(result.current.status).toBe("error"); @@ -876,7 +880,7 @@ describe("useDesktopConnection", () => { try { const { result, rerender } = renderHook( ({ chatId }: { chatId: string | undefined }) => - useDesktopConnection({ chatId }), + useDesktopConnection({ chatId, activated: true }), { initialProps: { chatId: "chat-aaa" as string | undefined } }, ); @@ -901,7 +905,9 @@ describe("useDesktopConnection", () => { // -- Visibility observer (ResizeObserver) --------------------------------- it("forces scaleViewport on hidden→visible transition", () => { - renderHook(() => useDesktopConnection({ chatId: "chat-1" })); + renderHook(() => + useDesktopConnection({ chatId: "chat-1", activated: true }), + ); const rfb = getLastRFBInstance(); act(() => rfb.simulateEvent("connect")); @@ -923,7 +929,9 @@ describe("useDesktopConnection", () => { }); it("does not force scaleViewport on normal nonzero→nonzero resize", () => { - renderHook(() => useDesktopConnection({ chatId: "chat-1" })); + renderHook(() => + useDesktopConnection({ chatId: "chat-1", activated: true }), + ); const rfb = getLastRFBInstance(); act(() => rfb.simulateEvent("connect")); @@ -943,7 +951,7 @@ describe("useDesktopConnection", () => { it("disconnects visibility observer on unmount", () => { const { unmount } = renderHook(() => - useDesktopConnection({ chatId: "chat-1" }), + useDesktopConnection({ chatId: "chat-1", activated: true }), ); getLastRFBInstance(); @@ -958,7 +966,9 @@ describe("useDesktopConnection", () => { vi.useFakeTimers(); try { - renderHook(() => useDesktopConnection({ chatId: "chat-1" })); + renderHook(() => + useDesktopConnection({ chatId: "chat-1", activated: true }), + ); const rfb1 = getLastRFBInstance(); act(() => rfb1.simulateEvent("connect")); @@ -982,7 +992,7 @@ describe("useDesktopConnection", () => { it("ignores stale visibility observer callback after chatId change", () => { const { rerender } = renderHook( ({ chatId }: { chatId: string | undefined }) => - useDesktopConnection({ chatId }), + useDesktopConnection({ chatId, activated: true }), { initialProps: { chatId: "chat-aaa" as string | undefined } }, ); diff --git a/site/src/pages/AgentsPage/hooks/useDesktopConnection.ts b/site/src/pages/AgentsPage/hooks/useDesktopConnection.ts index 006ca4dec8..8c9c7fa61c 100644 --- a/site/src/pages/AgentsPage/hooks/useDesktopConnection.ts +++ b/site/src/pages/AgentsPage/hooks/useDesktopConnection.ts @@ -6,6 +6,8 @@ import { useClipboard } from "#/hooks/useClipboard"; interface UseDesktopConnectionOptions { chatId: string | undefined; + /** When false the hook stays dormant — no WebSocket, no RFB. */ + activated: boolean; } type DesktopConnectionStatus = @@ -79,6 +81,7 @@ const isMacCutShortcut = (event: KeyboardEvent): boolean => { export function useDesktopConnection({ chatId, + activated, }: UseDesktopConnectionOptions): UseDesktopConnectionResult { const [status, setStatus] = useState("idle"); const [hasConnected, setHasConnected] = useState(false); @@ -485,7 +488,9 @@ export function useDesktopConnection({ doConnect(); }; - doConnect(); + if (activated) { + doConnect(); + } return () => { restartRef.current = null; @@ -493,7 +498,7 @@ export function useDesktopConnection({ setStatus("idle"); setHasConnected(false); }; - }, [chatId, syncRemoteClipboardToLocal]); + }, [activated, chatId, syncRemoteClipboardToLocal]); return { status,