11// @vitest -environment jsdom
22import { Panel , PanelGroup , PanelResizer } from "@window-splitter/react" ;
3- import { createElement , useEffect } from "react" ;
3+ import { createElement , useEffect , type ReactNode } from "react" ;
44import { createRoot , type Root } from "react-dom/client" ;
55import { act } from "react-dom/test-utils" ;
6- import { afterEach , describe , expect , it } from "vitest" ;
6+ import { afterEach , describe , expect , it , vi } from "vitest" ;
77import type { PanInfo } from "framer-motion" ;
88import { useDraggableResizable } from "~/components/primitives/DraggableResizable" ;
99import {
@@ -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.
242246function 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
347352describe ( "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 ( / \b 3 p x \b / ) ;
@@ -359,3 +364,71 @@ describe("DashboardAgent's degenerate grid tracks outside rightPanel", () => {
359364 expect ( group . style . gridTemplateColumns ) . toMatch ( / \b 3 p x \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+ } ) ;
0 commit comments