diff --git a/packages/dashboard/app/components/QuickChatFAB.tsx b/packages/dashboard/app/components/QuickChatFAB.tsx index 6cb115b625..94b9f078d3 100644 --- a/packages/dashboard/app/components/QuickChatFAB.tsx +++ b/packages/dashboard/app/components/QuickChatFAB.tsx @@ -340,6 +340,21 @@ const QUICK_CHAT_DEFAULT_PANEL_SIZE: PanelSize = { height: 400, }; +/** + * FNXC:QuickChatPanelSize 2026-06-16-23:03: + * FN-6502 requires Quick Chat to open taller by default on floating-panel mobile/tablet viewports while portrait mobile stays full-screen through CSS and desktop defaults plus persisted sizes remain unchanged. + */ +function getDefaultQuickChatPanelSize(): PanelSize { + if (typeof window === "undefined" || window.innerWidth <= QUICK_CHAT_DESKTOP_BREAKPOINT || window.innerWidth > 1024) { + return QUICK_CHAT_DEFAULT_PANEL_SIZE; + } + + return { + width: QUICK_CHAT_DEFAULT_PANEL_SIZE.width, + height: Math.max(QUICK_CHAT_DEFAULT_PANEL_SIZE.height, Math.floor(window.innerHeight * 0.8)), + }; +} + const ALLOWED_ATTACHMENT_TYPES = new Set([ "image/png", "image/jpeg", @@ -650,23 +665,30 @@ function usePanelResize(projectId: string | undefined, fabRight: number, fabBott ); const loadPersistedSize = useCallback((): PanelSize => { + const defaultSize = getDefaultQuickChatPanelSize(); if (typeof window === "undefined" || window.innerWidth <= QUICK_CHAT_DESKTOP_BREAKPOINT) { - return QUICK_CHAT_DEFAULT_PANEL_SIZE; + return defaultSize; } try { const raw = localStorage.getItem(storageKey); - if (!raw) return QUICK_CHAT_DEFAULT_PANEL_SIZE; + if (!raw) return defaultSize; const parsed = JSON.parse(raw) as Partial; if (typeof parsed.width !== "number" || typeof parsed.height !== "number") { - return QUICK_CHAT_DEFAULT_PANEL_SIZE; + return defaultSize; } return { width: parsed.width, height: parsed.height }; } catch { - return QUICK_CHAT_DEFAULT_PANEL_SIZE; + return defaultSize; } }, [storageKey]); const [panelSize, setPanelSize] = useState(loadPersistedSize); + const panelSizeRef = useRef(panelSize); + const hasUserResizedPanelRef = useRef(false); + + useEffect(() => { + panelSizeRef.current = panelSize; + }, [panelSize]); /** * Anchor offset relative to the FAB position. @@ -682,7 +704,7 @@ function usePanelResize(projectId: string | undefined, fabRight: number, fabBott }, [anchorOffset, clampPanelSize, fabBottom, fabRight, isDesktopViewport, isOpen]); useEffect(() => { - if (!isOpen || !isDesktopViewport()) return; + if (!isOpen || !isDesktopViewport() || !hasUserResizedPanelRef.current) return; try { localStorage.setItem(storageKey, JSON.stringify(panelSize)); } catch { @@ -772,6 +794,7 @@ function usePanelResize(projectId: string | undefined, fabRight: number, fabBott ), ); + hasUserResizedPanelRef.current = true; setPanelSize(clamped); setAnchorOffset({ right: clampedAnchorRight, bottom: clampedAnchorBottom }); }; @@ -786,7 +809,7 @@ function usePanelResize(projectId: string | undefined, fabRight: number, fabBott // Persist final size. try { - localStorage.setItem(storageKey, JSON.stringify({ width: panelSize.width, height: panelSize.height })); + localStorage.setItem(storageKey, JSON.stringify(panelSizeRef.current)); } catch { // Best-effort } @@ -802,8 +825,6 @@ function usePanelResize(projectId: string | undefined, fabRight: number, fabBott fabBottom, fabRight, isDesktopViewport, - panelSize.height, - panelSize.width, storageKey, ], ); diff --git a/packages/dashboard/app/components/__tests__/QuickChatFAB.test.tsx b/packages/dashboard/app/components/__tests__/QuickChatFAB.test.tsx index 59f4c4aafc..2acc7fbfa1 100644 --- a/packages/dashboard/app/components/__tests__/QuickChatFAB.test.tsx +++ b/packages/dashboard/app/components/__tests__/QuickChatFAB.test.tsx @@ -1455,6 +1455,79 @@ describe("QuickChatFAB session-first UX", () => { expect(panel).toHaveStyle({ width: "420px", height: "360px" }); }); + it("FN-6502: opens taller by default on tablet without persisting the computed size", async () => { + Object.defineProperty(window, "innerWidth", { configurable: true, value: 800 }); + Object.defineProperty(window, "innerHeight", { configurable: true, value: 900 }); + window.dispatchEvent(new Event("resize")); + mockUseViewportMode.mockReturnValue("tablet"); + + render(); + fireEvent.click(screen.getByTestId("quick-chat-fab")); + + const panel = await screen.findByTestId("quick-chat-panel"); + expect(panel).toHaveStyle({ width: "320px", height: "720px" }); + expect(localStorage.getItem("fusion:quick-chat-size-proj-1")).toBeNull(); + }); + + it("FN-6502: keeps the desktop default size unchanged when no persisted size exists", async () => { + Object.defineProperty(window, "innerWidth", { configurable: true, value: 1440 }); + Object.defineProperty(window, "innerHeight", { configurable: true, value: 900 }); + window.dispatchEvent(new Event("resize")); + mockUseViewportMode.mockReturnValue("desktop"); + + render(); + fireEvent.click(screen.getByTestId("quick-chat-fab")); + + const panel = await screen.findByTestId("quick-chat-panel"); + expect(panel).toHaveStyle({ width: "320px", height: "400px" }); + }); + + it("FN-6502: restores an existing desktop persisted size on desktop", async () => { + Object.defineProperty(window, "innerWidth", { configurable: true, value: 1440 }); + Object.defineProperty(window, "innerHeight", { configurable: true, value: 900 }); + window.dispatchEvent(new Event("resize")); + mockUseViewportMode.mockReturnValue("desktop"); + localStorage.setItem("fusion:quick-chat-size-proj-1", JSON.stringify({ width: 500, height: 520 })); + + render(); + fireEvent.click(screen.getByTestId("quick-chat-fab")); + + const panel = await screen.findByTestId("quick-chat-panel"); + expect(panel).toHaveStyle({ width: "500px", height: "520px" }); + }); + + it("FN-6502: tablet open does not overwrite a pre-existing desktop persisted size", async () => { + Object.defineProperty(window, "innerWidth", { configurable: true, value: 800 }); + Object.defineProperty(window, "innerHeight", { configurable: true, value: 900 }); + window.dispatchEvent(new Event("resize")); + mockUseViewportMode.mockReturnValue("tablet"); + const persistedSize = { width: 500, height: 520 }; + localStorage.setItem("fusion:quick-chat-size-proj-1", JSON.stringify(persistedSize)); + + render(); + fireEvent.click(screen.getByTestId("quick-chat-fab")); + + const panel = await screen.findByTestId("quick-chat-panel"); + expect(panel).toHaveStyle({ width: "500px", height: "520px" }); + expect(JSON.parse(localStorage.getItem("fusion:quick-chat-size-proj-1") || "null")).toEqual(persistedSize); + }); + + it("FN-6502: portrait mobile keeps inline panel sizing disabled for the full-screen CSS sheet", async () => { + Object.defineProperty(window, "innerWidth", { configurable: true, value: 375 }); + Object.defineProperty(window, "innerHeight", { configurable: true, value: 800 }); + window.dispatchEvent(new Event("resize")); + mockUseViewportMode.mockReturnValue("mobile"); + + render(); + fireEvent.click(screen.getByTestId("quick-chat-fab")); + + const panel = await screen.findByTestId("quick-chat-panel"); + expect(panel.style.width).toBe(""); + expect(panel.style.height).toBe(""); + expect(panel.style.right).toBe(""); + expect(panel.style.bottom).toBe(""); + }); + it("shows jump-to-latest only after leaving live tail and scrolls back on click", async () => { mockFetchChatMessages.mockResolvedValueOnce({ messages: [