FN-5856: preserve closed quick chat panel size

Prevent closed quick chat FAB drags from overwriting the saved desktop panel size.

- gate desktop panel resize clamping and persistence behind the panel open state
- pass the quick chat open state into the resize hook so closed FAB moves leave saved dimensions untouched
- add a regression test covering closed-FAB dragging and reopened panel dimensions
- add a patch changeset for the published CLI package

Files changed:
 .changeset/fn-5856-quick-chat-resize.md            |  5 +++++
 packages/dashboard/app/components/QuickChatFAB.tsx | 12 +++++------
 .../app/components/__tests__/QuickChatFAB.test.tsx | 23 ++++++++++++++++++++++
 3 files changed, 34 insertions(+), 6 deletions(-)

Fusion-Task-Id: FN-5856

Fusion-Task-Lineage: 3358f2b0-ae8d-468c-8e9e-71a1b5365061
This commit is contained in:
gsxdsm
2026-06-01 23:37:03 -07:00
parent b6c15fe470
commit c60dae1cb9
3 changed files with 34 additions and 6 deletions

View File

@@ -572,7 +572,7 @@ function useDraggable(projectId?: string, externalDidDragRef?: React.MutableRefO
};
}
function usePanelResize(projectId: string | undefined, fabRight: number, fabBottom: number) {
function usePanelResize(projectId: string | undefined, fabRight: number, fabBottom: number, isOpen: boolean) {
const storageKey = `fusion:quick-chat-size-${projectId || "default"}`;
const isDesktopViewport = useCallback(
@@ -631,19 +631,19 @@ function usePanelResize(projectId: string | undefined, fabRight: number, fabBott
const [anchorOffset, setAnchorOffset] = useState<PanelAnchorOffset>({ right: 0, bottom: 0 });
useEffect(() => {
if (!isDesktopViewport()) return;
if (!isOpen || !isDesktopViewport()) return;
const effective = { right: fabRight + anchorOffset.right, bottom: fabBottom + anchorOffset.bottom };
setPanelSize((current) => clampPanelSize(current, effective.right, effective.bottom));
}, [clampPanelSize, isDesktopViewport, fabRight, fabBottom, anchorOffset]);
}, [anchorOffset, clampPanelSize, fabBottom, fabRight, isDesktopViewport, isOpen]);
useEffect(() => {
if (!isDesktopViewport()) return;
if (!isOpen || !isDesktopViewport()) return;
try {
localStorage.setItem(storageKey, JSON.stringify(panelSize));
} catch {
// Ignore storage errors (private mode / quota)
}
}, [isDesktopViewport, panelSize, storageKey]);
}, [isDesktopViewport, isOpen, panelSize, storageKey]);
const handleResizeStart = useCallback(
(event: React.PointerEvent<HTMLDivElement>) => {
@@ -1003,7 +1003,7 @@ export function QuickChatFAB({
// Panel stays 60px above FAB (FAB is 48px tall + 12px gap)
const panelY = position.y + 60;
const { panelSize, anchorOffset, handleResizeStart } = usePanelResize(projectId, position.x, panelY);
const { panelSize, anchorOffset, handleResizeStart } = usePanelResize(projectId, position.x, panelY, isOpen);
const shouldApplyDesktopPanelSize = typeof window !== "undefined" && window.innerWidth > QUICK_CHAT_DESKTOP_BREAKPOINT;
// Chat session hook

View File

@@ -988,6 +988,29 @@ describe("QuickChatFAB session-first UX", () => {
expect(saved).toContain("\"y\"");
});
it("keeps persisted desktop panel size when dragging a closed FAB", async () => {
const persistedSize = { width: 420, height: 360 };
localStorage.setItem("fusion:quick-chat-size-proj-1", JSON.stringify(persistedSize));
render(<QuickChatFAB addToast={vi.fn()} projectId="proj-1" />);
const fab = screen.getByTestId("quick-chat-fab");
fireEvent.pointerDown(fab, { pointerId: 44, pointerType: "mouse", button: 0, clientX: 960, clientY: 700 });
fireEvent.pointerMove(document, { pointerId: 44, pointerType: "mouse", clientX: 900, clientY: 620 });
fireEvent.pointerUp(document, { pointerId: 44, pointerType: "mouse", clientX: 900, clientY: 620 });
expect(screen.queryByTestId("quick-chat-panel")).toBeNull();
expect(JSON.parse(localStorage.getItem("fusion:quick-chat-size-proj-1") || "null")).toEqual(persistedSize);
fireEvent.click(fab);
expect(screen.queryByTestId("quick-chat-panel")).toBeNull();
fireEvent.click(fab);
const panel = await screen.findByTestId("quick-chat-panel");
expect(panel).toHaveStyle({ width: "420px", height: "360px" });
});
it("shows jump-to-latest only after leaving live tail and scrolls back on click", async () => {
mockFetchChatMessages.mockResolvedValueOnce({
messages: [