@@ -113,7 +113,7 @@ type MockDraftLauncherProps = {
113113 dragState ?: {
114114 isActiveDropTarget : boolean ;
115115 isDragging : boolean ;
116- hoverPlacement : "center" | null ;
116+ hoverPlacement : PaneDropIntent [ "placement" ] | null ;
117117 } ;
118118 workspaceId : string ;
119119 paneId ?: string ;
@@ -168,14 +168,32 @@ vi.mock("./views/shared/draft-launcher", async () => {
168168const mockEditorPaneCard = vi . fn (
169169 ( {
170170 paneId,
171+ dragState,
171172 onClosePane,
173+ onPaneDragStart,
172174 } : {
173175 paneId : string ;
174176 workspaceId : string ;
177+ dragState ?: {
178+ isActiveDropTarget : boolean ;
179+ isDragging : boolean ;
180+ hoverPlacement : PaneDropIntent [ "placement" ] | null ;
181+ } ;
175182 onClosePane : ( paneId : string ) => void ;
183+ onPaneDragStart ?: ( source : PaneDragSourceSnapshot ) => void ;
176184 onSplitPane : ( paneId : string , direction : "horizontal" | "vertical" ) => void ;
177185 } ) => (
178- < div data-testid = { `editor-pane-${ paneId } ` } >
186+ < div
187+ data-testid = { `editor-pane-${ paneId } ` }
188+ data-dragging = { dragState ?. isDragging ? "true" : undefined }
189+ data-drop-target = { dragState ?. isActiveDropTarget ? "true" : undefined }
190+ data-hover-placement = { dragState ?. hoverPlacement ?? undefined }
191+ >
192+ { onPaneDragStart ? (
193+ < button type = "button" onPointerDown = { ( ) => onPaneDragStart ( { paneId, title : paneId } ) } >
194+ drag-{ paneId }
195+ </ button >
196+ ) : null }
179197 < button type = "button" onClick = { ( ) => onClosePane ( paneId ) } >
180198 close-editor-{ paneId }
181199 </ button >
@@ -187,7 +205,13 @@ vi.mock("./views/shared/editor-pane-card", () => ({
187205 EditorPaneCard : ( props : {
188206 paneId : string ;
189207 workspaceId : string ;
208+ dragState ?: {
209+ isActiveDropTarget : boolean ;
210+ isDragging : boolean ;
211+ hoverPlacement : PaneDropIntent [ "placement" ] | null ;
212+ } ;
190213 onClosePane : ( paneId : string ) => void ;
214+ onPaneDragStart ?: ( source : PaneDragSourceSnapshot ) => void ;
191215 onSplitPane : ( paneId : string , direction : "horizontal" | "vertical" ) => void ;
192216 } ) => mockEditorPaneCard ( props ) ,
193217} ) ) ;
@@ -586,7 +610,7 @@ describe("AgentPanes", () => {
586610 ) ;
587611 } ) ;
588612
589- it ( "moves a session into a draft pane on a center drop over a draft target" , async ( ) => {
613+ it ( "swaps a session with a draft pane on a center drop over a draft target" , async ( ) => {
590614 const sendCommand = vi . fn ( async ( op : string , args ?: Record < string , unknown > ) => {
591615 if ( op === "session.list" ) {
592616 return [
@@ -644,9 +668,14 @@ describe("AgentPanes", () => {
644668
645669 await waitFor ( ( ) => {
646670 expect ( store . get ( paneLayoutAtomFamily ( "ws-1" ) ) ) . toEqual ( {
647- id : "right" ,
648- type : "leaf" ,
649- sessionId : "sess_1" ,
671+ id : "root" ,
672+ type : "split" ,
673+ direction : "horizontal" ,
674+ ratio : 0.5 ,
675+ children : [
676+ { id : "left" , type : "leaf" } ,
677+ { id : "right" , type : "leaf" , sessionId : "sess_1" } ,
678+ ] ,
650679 } ) ;
651680 } ) ;
652681
@@ -656,9 +685,14 @@ describe("AgentPanes", () => {
656685 workspaceId : "ws-1" ,
657686 uiState : expect . objectContaining ( {
658687 paneLayout : {
659- id : "right" ,
660- type : "leaf" ,
661- sessionId : "sess_1" ,
688+ id : "root" ,
689+ type : "split" ,
690+ direction : "horizontal" ,
691+ ratio : 0.5 ,
692+ children : [
693+ { id : "left" , type : "leaf" } ,
694+ { id : "right" , type : "leaf" , sessionId : "sess_1" } ,
695+ ] ,
662696 } ,
663697 } ) ,
664698 } ) ,
@@ -865,6 +899,103 @@ describe("AgentPanes", () => {
865899 expect ( document . body ) . not . toHaveClass ( "is-dragging-pane" ) ;
866900 } ) ;
867901
902+ it ( "registers editor pane wrappers as drop targets and swaps with a session through pointer drag" , async ( ) => {
903+ const { store } = createAgentPaneStore ( {
904+ id : "root" ,
905+ type : "split" ,
906+ direction : "horizontal" ,
907+ ratio : 0.5 ,
908+ children : [
909+ { id : "left" , type : "leaf" , leafKind : "session" , sessionId : "sess_1" } ,
910+ { id : "right" , type : "leaf" , leafKind : "editor" } ,
911+ ] ,
912+ } ) ;
913+ store . set ( activeEditorPaneIdAtomFamily ( "ws-1" ) , "right" ) ;
914+ store . set ( focusedEditorPaneIdAtomFamily ( "ws-1" ) , "right" ) ;
915+
916+ render (
917+ < Provider store = { store } >
918+ < AgentPanes hydrateSessions = { false } />
919+ </ Provider >
920+ ) ;
921+
922+ const editorPane = setPaneRect ( "right" , { left : 260 , top : 0 , width : 220 , height : 180 } ) ;
923+ setPaneRect ( "left" , { left : 0 , top : 0 , width : 220 , height : 180 } ) ;
924+
925+ fireEvent . pointerDown ( screen . getByRole ( "button" , { name : "drag-sess_1" } ) ) ;
926+ fireEvent . pointerMove ( window , { clientX : 370 , clientY : 90 } ) ;
927+
928+ await waitFor ( ( ) => {
929+ expect ( editorPane ) . toHaveAttribute ( "data-pane-drop-target" , "true" ) ;
930+ expect ( editorPane ) . toHaveAttribute ( "data-pane-hover-placement" , "center" ) ;
931+ } ) ;
932+
933+ fireEvent . pointerUp ( window , { clientX : 370 , clientY : 90 } ) ;
934+
935+ await waitFor ( ( ) => {
936+ expect ( store . get ( paneLayoutAtomFamily ( "ws-1" ) ) ) . toEqual ( {
937+ id : "root" ,
938+ type : "split" ,
939+ direction : "horizontal" ,
940+ ratio : 0.5 ,
941+ children : [
942+ { id : "left" , type : "leaf" , leafKind : "editor" } ,
943+ { id : "right" , type : "leaf" , leafKind : "session" , sessionId : "sess_1" } ,
944+ ] ,
945+ } ) ;
946+ expect ( store . get ( activeEditorPaneIdAtomFamily ( "ws-1" ) ) ) . toBe ( "left" ) ;
947+ } ) ;
948+ } ) ;
949+
950+ it ( "keeps editor focus attached when the editor pane is dragged over a session" , async ( ) => {
951+ const { store } = createAgentPaneStore ( {
952+ id : "root" ,
953+ type : "split" ,
954+ direction : "horizontal" ,
955+ ratio : 0.5 ,
956+ children : [
957+ { id : "left" , type : "leaf" , leafKind : "editor" } ,
958+ { id : "right" , type : "leaf" , leafKind : "session" , sessionId : "sess_1" } ,
959+ ] ,
960+ } ) ;
961+ store . set ( activeEditorPaneIdAtomFamily ( "ws-1" ) , "left" ) ;
962+ store . set ( focusedEditorPaneIdAtomFamily ( "ws-1" ) , "left" ) ;
963+
964+ render (
965+ < Provider store = { store } >
966+ < AgentPanes hydrateSessions = { false } />
967+ </ Provider >
968+ ) ;
969+
970+ setPaneRect ( "left" , { left : 0 , top : 0 , width : 220 , height : 180 } ) ;
971+ const sessionPane = setPaneRect ( "right" , { left : 260 , top : 0 , width : 220 , height : 180 } ) ;
972+
973+ fireEvent . pointerDown ( screen . getByRole ( "button" , { name : "drag-left" } ) ) ;
974+ fireEvent . pointerMove ( window , { clientX : 370 , clientY : 90 } ) ;
975+
976+ await waitFor ( ( ) => {
977+ expect ( sessionPane ) . toHaveAttribute ( "data-pane-drop-target" , "true" ) ;
978+ expect ( sessionPane ) . toHaveAttribute ( "data-pane-hover-placement" , "center" ) ;
979+ } ) ;
980+
981+ fireEvent . pointerUp ( window , { clientX : 370 , clientY : 90 } ) ;
982+
983+ await waitFor ( ( ) => {
984+ expect ( store . get ( paneLayoutAtomFamily ( "ws-1" ) ) ) . toEqual ( {
985+ id : "root" ,
986+ type : "split" ,
987+ direction : "horizontal" ,
988+ ratio : 0.5 ,
989+ children : [
990+ { id : "left" , type : "leaf" , leafKind : "session" , sessionId : "sess_1" } ,
991+ { id : "right" , type : "leaf" , leafKind : "editor" } ,
992+ ] ,
993+ } ) ;
994+ expect ( store . get ( activeEditorPaneIdAtomFamily ( "ws-1" ) ) ) . toBe ( "right" ) ;
995+ expect ( store . get ( focusedEditorPaneIdAtomFamily ( "ws-1" ) ) ) . toBe ( "right" ) ;
996+ } ) ;
997+ } ) ;
998+
868999 it ( "keeps the remaining draft pane visible after closing the last session pane" , async ( ) => {
8691000 const { store } = createAgentPaneStore ( {
8701001 id : "root" ,
0 commit comments