11import type { FileState , Terminal , AcpSession } from '../../types' ;
2- import { WheelEvent , useState , useMemo } from 'react' ;
2+ import { WheelEvent , useState , useMemo , useEffect } from 'react' ;
33import { loadItem , saveItem } from '../../storage' ;
44import { TabContextMenu } from './TabContextMenu' ;
55import type { TabMenuAction } from './TabContextMenu' ;
@@ -11,6 +11,10 @@ const PINNED_FILES_KEY = 'pinnedFiles';
1111const PINNED_TERMINALS_KEY = 'pinnedTerminals' ;
1212const PINNED_AGENTS_KEY = 'pinnedAgents' ;
1313
14+ const FILE_IDS_ORDER_KEY = 'toolbarFileIdsOrder' ;
15+ const TERMINAL_IDS_ORDER_KEY = 'toolbarTerminalIdsOrder' ;
16+ const AGENT_IDS_ORDER_KEY = 'toolbarAgentIdsOrder' ;
17+
1418interface ToolbarProps {
1519 files : FileState [ ] ;
1620 activeFileId : string | null ;
@@ -56,6 +60,33 @@ export const Toolbar = ({
5660 return loadItem < string [ ] > ( PINNED_AGENTS_KEY ) ?? [ ] ;
5761 } ) ;
5862
63+ const [ fileIdsOrder , setFileIdsOrder ] = useState < string [ ] > ( ( ) => {
64+ return loadItem < string [ ] > ( FILE_IDS_ORDER_KEY ) ?? [ ] ;
65+ } ) ;
66+ const [ terminalIdsOrder , setTerminalIdsOrder ] = useState < string [ ] > ( ( ) => {
67+ return loadItem < string [ ] > ( TERMINAL_IDS_ORDER_KEY ) ?? [ ] ;
68+ } ) ;
69+ const [ agentIdsOrder , setAgentIdsOrder ] = useState < string [ ] > ( ( ) => {
70+ return loadItem < string [ ] > ( AGENT_IDS_ORDER_KEY ) ?? [ ] ;
71+ } ) ;
72+
73+ const [ draggedItem , setDraggedItem ] = useState < {
74+ type : 'file' | 'terminal' | 'agent' ;
75+ id : string ;
76+ } | null > ( null ) ;
77+
78+ useEffect ( ( ) => {
79+ saveItem ( FILE_IDS_ORDER_KEY , fileIdsOrder ) ;
80+ } , [ fileIdsOrder ] ) ;
81+
82+ useEffect ( ( ) => {
83+ saveItem ( TERMINAL_IDS_ORDER_KEY , terminalIdsOrder ) ;
84+ } , [ terminalIdsOrder ] ) ;
85+
86+ useEffect ( ( ) => {
87+ saveItem ( AGENT_IDS_ORDER_KEY , agentIdsOrder ) ;
88+ } , [ agentIdsOrder ] ) ;
89+
5990 const togglePinFile = ( fileId : string ) => {
6091 setPinnedFileIds ( ( prev ) => {
6192 const next = prev . includes ( fileId )
@@ -87,31 +118,153 @@ export const Toolbar = ({
87118 } ;
88119
89120 const sortedFiles = useMemo ( ( ) => {
90- const pinned = pinnedFileIds
91- . map ( ( id ) => files . find ( ( f ) => f . id === id ) )
92- . filter ( ( f ) : f is FileState => ! ! f ) ;
93- const pinnedSet = new Set ( pinnedFileIds ) ;
94- const unpinned = files . filter ( ( f ) => ! pinnedSet . has ( f . id ) ) ;
95- return [ ...pinned , ...unpinned ] ;
96- } , [ files , pinnedFileIds ] ) ;
121+ const pinned = files . filter ( ( f ) => pinnedFileIds . includes ( f . id ) ) ;
122+ const unpinned = files . filter ( ( f ) => ! pinnedFileIds . includes ( f . id ) ) ;
123+
124+ const sortFunc = ( a : FileState , b : FileState ) => {
125+ const indexA = fileIdsOrder . indexOf ( a . id ) ;
126+ const indexB = fileIdsOrder . indexOf ( b . id ) ;
127+ if ( indexA === - 1 && indexB === - 1 ) return 0 ;
128+ if ( indexA === - 1 ) return 1 ;
129+ if ( indexB === - 1 ) return - 1 ;
130+ return indexA - indexB ;
131+ } ;
132+
133+ return [ ...pinned . sort ( sortFunc ) , ...unpinned . sort ( sortFunc ) ] ;
134+ } , [ files , pinnedFileIds , fileIdsOrder ] ) ;
97135
98136 const sortedTerminals = useMemo ( ( ) => {
99- const pinned = pinnedTerminalIds
100- . map ( ( id ) => terminals . find ( ( t ) => t . id === id ) )
101- . filter ( ( t ) : t is Terminal => ! ! t ) ;
102- const pinnedSet = new Set ( pinnedTerminalIds ) ;
103- const unpinned = terminals . filter ( ( t ) => ! pinnedSet . has ( t . id ) ) ;
104- return [ ...pinned , ...unpinned ] ;
105- } , [ terminals , pinnedTerminalIds ] ) ;
137+ const pinned = terminals . filter ( ( t ) => pinnedTerminalIds . includes ( t . id ) ) ;
138+ const unpinned = terminals . filter ( ( t ) => ! pinnedTerminalIds . includes ( t . id ) ) ;
139+
140+ const sortFunc = ( a : Terminal , b : Terminal ) => {
141+ const indexA = terminalIdsOrder . indexOf ( a . id ) ;
142+ const indexB = terminalIdsOrder . indexOf ( b . id ) ;
143+ if ( indexA === - 1 && indexB === - 1 ) return 0 ;
144+ if ( indexA === - 1 ) return 1 ;
145+ if ( indexB === - 1 ) return - 1 ;
146+ return indexA - indexB ;
147+ } ;
148+
149+ return [ ...pinned . sort ( sortFunc ) , ...unpinned . sort ( sortFunc ) ] ;
150+ } , [ terminals , pinnedTerminalIds , terminalIdsOrder ] ) ;
106151
107152 const sortedAgentSessions = useMemo ( ( ) => {
108- const pinned = pinnedAgentIds
109- . map ( ( id ) => agentSessions . find ( ( s ) => s . agentId === id ) )
110- . filter ( ( s ) : s is AcpSession => ! ! s ) ;
111- const pinnedSet = new Set ( pinnedAgentIds ) ;
112- const unpinned = agentSessions . filter ( ( s ) => ! pinnedSet . has ( s . agentId ) ) ;
113- return [ ...pinned , ...unpinned ] ;
114- } , [ agentSessions , pinnedAgentIds ] ) ;
153+ const pinned = agentSessions . filter ( ( s ) => pinnedAgentIds . includes ( s . agentId ) ) ;
154+ const unpinned = agentSessions . filter ( ( s ) => ! pinnedAgentIds . includes ( s . agentId ) ) ;
155+
156+ const sortFunc = ( a : AcpSession , b : AcpSession ) => {
157+ const indexA = agentIdsOrder . indexOf ( a . agentId ) ;
158+ const indexB = agentIdsOrder . indexOf ( b . agentId ) ;
159+ if ( indexA === - 1 && indexB === - 1 ) return 0 ;
160+ if ( indexA === - 1 ) return 1 ;
161+ if ( indexB === - 1 ) return - 1 ;
162+ return indexA - indexB ;
163+ } ;
164+
165+ return [ ...pinned . sort ( sortFunc ) , ...unpinned . sort ( sortFunc ) ] ;
166+ } , [ agentSessions , pinnedAgentIds , agentIdsOrder ] ) ;
167+
168+ const handleDragStart = ( e : React . DragEvent , type : 'file' | 'terminal' | 'agent' , id : string ) => {
169+ setDraggedItem ( { type, id } ) ;
170+ e . dataTransfer . effectAllowed = 'move' ;
171+ } ;
172+
173+ const handleDragEnd = ( ) => {
174+ setDraggedItem ( null ) ;
175+ } ;
176+
177+ const handleDrop = ( e : React . DragEvent ) => {
178+ e . preventDefault ( ) ;
179+ } ;
180+
181+ const handleDragOver = ( e : React . DragEvent , type : 'file' | 'terminal' | 'agent' , targetId : string ) => {
182+ if ( ! draggedItem || draggedItem . type !== type || draggedItem . id === targetId ) {
183+ return ;
184+ }
185+
186+ e . preventDefault ( ) ;
187+
188+ if ( type === 'file' ) {
189+ const isDraggedPinned = pinnedFileIds . includes ( draggedItem . id ) ;
190+ const isTargetPinned = pinnedFileIds . includes ( targetId ) ;
191+
192+ if ( isDraggedPinned !== isTargetPinned ) {
193+ return ;
194+ }
195+
196+ setFileIdsOrder ( ( prev ) => {
197+ let nextOrder = [ ...prev ] ;
198+ const currentSortedIds = sortedFiles . map ( ( f ) => f . id ) ;
199+ currentSortedIds . forEach ( ( id ) => {
200+ if ( ! nextOrder . includes ( id ) ) {
201+ nextOrder . push ( id ) ;
202+ }
203+ } ) ;
204+
205+ const fromIndex = nextOrder . indexOf ( draggedItem . id ) ;
206+ const toIndex = nextOrder . indexOf ( targetId ) ;
207+
208+ if ( fromIndex !== - 1 && toIndex !== - 1 && fromIndex !== toIndex ) {
209+ nextOrder . splice ( fromIndex , 1 ) ;
210+ nextOrder . splice ( toIndex , 0 , draggedItem . id ) ;
211+ }
212+ return nextOrder ;
213+ } ) ;
214+ } else if ( type === 'terminal' ) {
215+ const isDraggedPinned = pinnedTerminalIds . includes ( draggedItem . id ) ;
216+ const isTargetPinned = pinnedTerminalIds . includes ( targetId ) ;
217+
218+ if ( isDraggedPinned !== isTargetPinned ) {
219+ return ;
220+ }
221+
222+ setTerminalIdsOrder ( ( prev ) => {
223+ let nextOrder = [ ...prev ] ;
224+ const currentSortedIds = sortedTerminals . map ( ( t ) => t . id ) ;
225+ currentSortedIds . forEach ( ( id ) => {
226+ if ( ! nextOrder . includes ( id ) ) {
227+ nextOrder . push ( id ) ;
228+ }
229+ } ) ;
230+
231+ const fromIndex = nextOrder . indexOf ( draggedItem . id ) ;
232+ const toIndex = nextOrder . indexOf ( targetId ) ;
233+
234+ if ( fromIndex !== - 1 && toIndex !== - 1 && fromIndex !== toIndex ) {
235+ nextOrder . splice ( fromIndex , 1 ) ;
236+ nextOrder . splice ( toIndex , 0 , draggedItem . id ) ;
237+ }
238+ return nextOrder ;
239+ } ) ;
240+ } else if ( type === 'agent' ) {
241+ const isDraggedPinned = pinnedAgentIds . includes ( draggedItem . id ) ;
242+ const isTargetPinned = pinnedAgentIds . includes ( targetId ) ;
243+
244+ if ( isDraggedPinned !== isTargetPinned ) {
245+ return ;
246+ }
247+
248+ setAgentIdsOrder ( ( prev ) => {
249+ let nextOrder = [ ...prev ] ;
250+ const currentSortedIds = sortedAgentSessions . map ( ( s ) => s . agentId ) ;
251+ currentSortedIds . forEach ( ( id ) => {
252+ if ( ! nextOrder . includes ( id ) ) {
253+ nextOrder . push ( id ) ;
254+ }
255+ } ) ;
256+
257+ const fromIndex = nextOrder . indexOf ( draggedItem . id ) ;
258+ const toIndex = nextOrder . indexOf ( targetId ) ;
259+
260+ if ( fromIndex !== - 1 && toIndex !== - 1 && fromIndex !== toIndex ) {
261+ nextOrder . splice ( fromIndex , 1 ) ;
262+ nextOrder . splice ( toIndex , 0 , draggedItem . id ) ;
263+ }
264+ return nextOrder ;
265+ } ) ;
266+ }
267+ } ;
115268
116269 // Mass tab closing helper handlers
117270 const handleCloseRightFiles = ( fileId : string ) => {
@@ -275,6 +428,12 @@ export const Toolbar = ({
275428 onSelect = { ( ) => onSelectFile ( file . id ) }
276429 onClose = { ( ) => onCloseFile ( file . id ) }
277430 onContextMenu = { ( event ) => openMenu ( event , 'file' , file . id ) }
431+ draggable = { true }
432+ dragging = { draggedItem ?. type === 'file' && draggedItem ?. id === file . id }
433+ onDragStart = { ( event ) => handleDragStart ( event , 'file' , file . id ) }
434+ onDragEnd = { handleDragEnd }
435+ onDragOver = { ( event ) => handleDragOver ( event , 'file' , file . id ) }
436+ onDrop = { handleDrop }
278437 />
279438 ) ) }
280439 { sortedTerminals . map ( ( terminal ) => (
@@ -288,6 +447,12 @@ export const Toolbar = ({
288447 onSelect = { ( ) => onSelectTerminal ( terminal . id ) }
289448 onClose = { ( ) => onCloseTerminal ( terminal . id ) }
290449 onContextMenu = { ( event ) => openMenu ( event , 'terminal' , terminal . id ) }
450+ draggable = { true }
451+ dragging = { draggedItem ?. type === 'terminal' && draggedItem ?. id === terminal . id }
452+ onDragStart = { ( event ) => handleDragStart ( event , 'terminal' , terminal . id ) }
453+ onDragEnd = { handleDragEnd }
454+ onDragOver = { ( event ) => handleDragOver ( event , 'terminal' , terminal . id ) }
455+ onDrop = { handleDrop }
291456 />
292457 ) ) }
293458 { sortedAgentSessions . map ( ( session ) => (
@@ -301,6 +466,12 @@ export const Toolbar = ({
301466 onSelect = { ( ) => onSelectAgent ( session . agentId ) }
302467 onClose = { ( ) => onCloseAgent ( session . agentId ) }
303468 onContextMenu = { ( event ) => openMenu ( event , 'agent' , session . agentId ) }
469+ draggable = { true }
470+ dragging = { draggedItem ?. type === 'agent' && draggedItem ?. id === session . agentId }
471+ onDragStart = { ( event ) => handleDragStart ( event , 'agent' , session . agentId ) }
472+ onDragEnd = { handleDragEnd }
473+ onDragOver = { ( event ) => handleDragOver ( event , 'agent' , session . agentId ) }
474+ onDrop = { handleDrop }
304475 />
305476 ) ) }
306477 </ div >
0 commit comments