Skip to content

Commit 599ee59

Browse files
committed
fix(webapp): keep resize handle mounted so mode switches never remount the chat
1 parent 5ff2924 commit 599ee59

4 files changed

Lines changed: 95 additions & 10 deletions

File tree

‎apps/webapp/app/components/dashboard-agent/DashboardAgent.tsx‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type { SuggestedPrompt, WatchSpec } from "@internal/dashboard-agent-contr
22
import { useLocation } from "@remix-run/react";
33
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
44
import {
5+
collapsibleHandleClassName,
56
ResizableHandle,
67
ResizablePanel,
78
ResizablePanelGroup,
@@ -363,7 +364,11 @@ export function DashboardAgent({
363364
<ResizablePanel id="dashboard-content" min="320px">
364365
<div className={agentHiddenContentClassName(fullscreen)}>{children}</div>
365366
</ResizablePanel>
366-
{mode === "rightPanel" && <ResizableHandle id="dashboard-agent-handle" />}
367+
<ResizableHandle
368+
id="dashboard-agent-handle"
369+
size={mode === "rightPanel" ? "3px" : "0px"}
370+
className={collapsibleHandleClassName(mode === "rightPanel")}
371+
/>
367372
<ResizablePanel
368373
id="dashboard-agent-panel"
369374
default="380px"

‎apps/webapp/app/components/dashboard-agent/floating-window-mode.test.ts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,13 @@ describe("DashboardAgent.tsx keeps one tree shape across display modes", () => {
2525

2626
it('gates the right-column sizing on mode === "rightPanel", not a branch around FloatingAgentWindow', () => {
2727
expect(source).toContain('collapsed={mode !== "rightPanel"}');
28-
expect(source).toContain('{mode === "rightPanel" && <ResizableHandle');
28+
});
29+
30+
it("keeps ResizableHandle always mounted — a conditional handle shifts sibling keys and remounts the chat", () => {
31+
const occurrences = source.match(/<ResizableHandle\b/g) ?? [];
32+
expect(occurrences).toHaveLength(1);
33+
expect(source).not.toMatch(/mode === "rightPanel" &&\s*<ResizableHandle/);
34+
expect(source).toContain('size={mode === "rightPanel" ? "3px" : "0px"}');
2935
});
3036

3137
it("unclips the degenerate panel with Tailwind v4's trailing-bang important modifier", () => {

‎apps/webapp/app/components/dashboard-agent/panel-layout.dom.test.ts‎

Lines changed: 80 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
// @vitest-environment jsdom
22
import { Panel, PanelGroup, PanelResizer } from "@window-splitter/react";
3-
import { createElement, useEffect } from "react";
3+
import { createElement, useEffect, type ReactNode } from "react";
44
import { createRoot, type Root } from "react-dom/client";
55
import { act } from "react-dom/test-utils";
6-
import { afterEach, describe, expect, it } from "vitest";
6+
import { afterEach, describe, expect, it, vi } from "vitest";
77
import type { PanInfo } from "framer-motion";
88
import { useDraggableResizable } from "~/components/primitives/DraggableResizable";
99
import {
@@ -239,6 +239,10 @@ describe("FloatingAgentWindow's fullscreen geometry", () => {
239239
// and an agent panel that's either sized (rightPanel) or truly collapsed (otherwise).
240240
// A leftover fixed-pixel track from a hidden-not-unmounted handle, or from a "0px" panel
241241
// that doesn't actually collapse, pushes the grid past its own container's width.
242+
// The handle is ALWAYS mounted (only its `size` varies) — PanelGroup keys children by
243+
// index after dropping falsy ones (@window-splitter/react's useIndexedChildren), so a
244+
// conditionally-rendered handle shifts the agent panel's key on every mode switch and
245+
// remounts the whole chat subtree beneath it.
242246
function renderDashboardAgentGrid(rightPanel: boolean) {
243247
container = document.createElement("div");
244248
document.body.appendChild(container);
@@ -249,9 +253,10 @@ function renderDashboardAgentGrid(rightPanel: boolean) {
249253
PanelGroup,
250254
{ orientation: "horizontal" },
251255
createElement(Panel, { id: "dashboard-content", min: "320px" }),
252-
rightPanel
253-
? createElement(PanelResizer, { id: "dashboard-agent-handle", size: "3px" })
254-
: null,
256+
createElement(PanelResizer, {
257+
id: "dashboard-agent-handle",
258+
size: rightPanel ? "3px" : "0px",
259+
}),
255260
createElement(Panel, {
256261
id: "dashboard-agent-panel",
257262
default: "380px",
@@ -345,9 +350,9 @@ describe("FloatingAgentWindow clears floating geometry when it docks", () => {
345350
});
346351

347352
describe("DashboardAgent's degenerate grid tracks outside rightPanel", () => {
348-
it("unmounts the handle and collapses the agent panel to a bare 0px track", () => {
353+
it("keeps the handle mounted but collapses it and the agent panel to bare 0px tracks", () => {
349354
const group = renderDashboardAgentGrid(false);
350-
expect(group.querySelector('[data-splitter-type="handle"]')).toBeNull();
355+
expect(group.querySelector('[data-splitter-type="handle"]')).not.toBeNull();
351356
const columns = group.style.gridTemplateColumns;
352357
expect(columns.endsWith("0px")).toBe(true);
353358
expect(columns).not.toMatch(/\b3px\b/);
@@ -359,3 +364,71 @@ describe("DashboardAgent's degenerate grid tracks outside rightPanel", () => {
359364
expect(group.style.gridTemplateColumns).toMatch(/\b3px\b/);
360365
});
361366
});
367+
368+
// Mirrors DashboardAgent.tsx's real PanelGroup/Panel/PanelResizer shape (not the
369+
// FloatingAgentWindow-standalone test above, which never puts a handle between the
370+
// panels and so is blind to the sibling-key-shift bug this pins).
371+
function renderDashboardAgentGridTree(rightPanel: boolean, agentChild: ReactNode) {
372+
return createElement(
373+
PanelGroup,
374+
{ orientation: "horizontal" },
375+
createElement(Panel, { id: "dashboard-content", min: "320px" }),
376+
createElement(PanelResizer, {
377+
id: "dashboard-agent-handle",
378+
size: rightPanel ? "3px" : "0px",
379+
}),
380+
createElement(
381+
Panel,
382+
{
383+
id: "dashboard-agent-panel",
384+
default: "380px",
385+
min: "320px",
386+
max: "720px",
387+
collapsible: true,
388+
collapsed: !rightPanel,
389+
collapsedSize: "0px",
390+
},
391+
agentChild
392+
)
393+
);
394+
}
395+
396+
describe("DashboardAgent's real grid tree never remounts the chat across mode switches", () => {
397+
it("keeps the agent panel's child mounted across floating/rightPanel/fullscreen transitions", () => {
398+
let mounts = 0;
399+
function Marker() {
400+
useEffect(() => {
401+
mounts += 1;
402+
}, []);
403+
return null;
404+
}
405+
406+
// jsdom reports every rect as 0x0; the library divides by the group's measured
407+
// width when a mode switch changes a panel's size, so it needs a non-zero stand-in.
408+
const rectSpy = vi
409+
.spyOn(HTMLElement.prototype, "getBoundingClientRect")
410+
.mockReturnValue({ width: 1000, height: 600, x: 0, y: 0, top: 0, left: 0 } as DOMRect);
411+
412+
container = document.createElement("div");
413+
document.body.appendChild(container);
414+
root = createRoot(container);
415+
416+
const sequence: DashboardAgentMode[] = [
417+
"floating",
418+
"rightPanel",
419+
"floating",
420+
"fullscreen",
421+
"rightPanel",
422+
];
423+
try {
424+
for (const mode of sequence) {
425+
act(() => {
426+
root!.render(renderDashboardAgentGridTree(mode === "rightPanel", createElement(Marker)));
427+
});
428+
expect(mounts).toBe(1);
429+
}
430+
} finally {
431+
rectSpy.mockRestore();
432+
}
433+
});
434+
});

‎apps/webapp/app/components/dashboard-agent/panel-layout.tsx‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ const AGENT_MODE_STORAGE_KEY = "tdev:dashboard-agent:mode";
2525

2626
export type DashboardAgentMode = "floating" | "rightPanel" | "fullscreen";
2727

28-
// V1 floating window: 380x512, bottom-right, matching the gallery's own panel frame.
28+
// V1 floating window: FLOATING_WIDTH x FLOATING_HEIGHT, bottom-right, matching the
29+
// gallery's own panel frame.
2930
export const FLOATING_WIDTH = 380;
3031
export const FLOATING_HEIGHT = 600;
3132
export const FLOATING_MARGIN = 16;

0 commit comments

Comments
 (0)