11import type { FileState , Terminal , AcpSession } from '../../types' ;
2- import type { WheelEvent } from 'react' ;
2+ import { WheelEvent , useState , useMemo } from 'react' ;
3+ import { loadItem , saveItem } from '../../storage' ;
34import { TabContextMenu } from './TabContextMenu' ;
45import type { TabMenuAction } from './TabContextMenu' ;
56import { ToolbarTab } from './ToolbarTab' ;
67import { useTabContextMenu } from './useTabContextMenu' ;
78import './Toolbar.css' ;
89
10+ const PINNED_FILES_KEY = 'pinnedFiles' ;
11+ const PINNED_TERMINALS_KEY = 'pinnedTerminals' ;
12+ const PINNED_AGENTS_KEY = 'pinnedAgents' ;
13+
914interface ToolbarProps {
1015 files : FileState [ ] ;
1116 activeFileId : string | null ;
@@ -40,7 +45,122 @@ export const Toolbar = ({
4045 onCloseAgent,
4146} : ToolbarProps ) => {
4247 const { closeMenu, menuRef, openMenu, tabMenu } = useTabContextMenu ( ) ;
43-
48+
49+ const [ pinnedFileIds , setPinnedFileIds ] = useState < string [ ] > ( ( ) => {
50+ return loadItem < string [ ] > ( PINNED_FILES_KEY ) ?? [ ] ;
51+ } ) ;
52+ const [ pinnedTerminalIds , setPinnedTerminalIds ] = useState < string [ ] > ( ( ) => {
53+ return loadItem < string [ ] > ( PINNED_TERMINALS_KEY ) ?? [ ] ;
54+ } ) ;
55+ const [ pinnedAgentIds , setPinnedAgentIds ] = useState < string [ ] > ( ( ) => {
56+ return loadItem < string [ ] > ( PINNED_AGENTS_KEY ) ?? [ ] ;
57+ } ) ;
58+
59+ const togglePinFile = ( fileId : string ) => {
60+ setPinnedFileIds ( ( prev ) => {
61+ const next = prev . includes ( fileId )
62+ ? prev . filter ( ( id ) => id !== fileId )
63+ : [ ...prev , fileId ] ;
64+ saveItem ( PINNED_FILES_KEY , next ) ;
65+ return next ;
66+ } ) ;
67+ } ;
68+
69+ const togglePinTerminal = ( terminalId : string ) => {
70+ setPinnedTerminalIds ( ( prev ) => {
71+ const next = prev . includes ( terminalId )
72+ ? prev . filter ( ( id ) => id !== terminalId )
73+ : [ ...prev , terminalId ] ;
74+ saveItem ( PINNED_TERMINALS_KEY , next ) ;
75+ return next ;
76+ } ) ;
77+ } ;
78+
79+ const togglePinAgent = ( agentId : string ) => {
80+ setPinnedAgentIds ( ( prev ) => {
81+ const next = prev . includes ( agentId )
82+ ? prev . filter ( ( id ) => id !== agentId )
83+ : [ ...prev , agentId ] ;
84+ saveItem ( PINNED_AGENTS_KEY , next ) ;
85+ return next ;
86+ } ) ;
87+ } ;
88+
89+ 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 ] ) ;
97+
98+ 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 ] ) ;
106+
107+ 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 ] ) ;
115+
116+ // Mass tab closing helper handlers
117+ const handleCloseRightFiles = ( fileId : string ) => {
118+ const fileIndexSorted = sortedFiles . findIndex ( ( f ) => f . id === fileId ) ;
119+ if ( fileIndexSorted < 0 ) return ;
120+ sortedFiles . slice ( fileIndexSorted + 1 ) . forEach ( ( f ) => {
121+ if ( ! pinnedFileIds . includes ( f . id ) ) {
122+ onCloseFile ( f . id ) ;
123+ }
124+ } ) ;
125+ } ;
126+
127+ const handleCloseAllFiles = ( ) => {
128+ files . forEach ( ( f ) => {
129+ if ( ! pinnedFileIds . includes ( f . id ) ) {
130+ onCloseFile ( f . id ) ;
131+ }
132+ } ) ;
133+ } ;
134+
135+ const handleCloseRightTerminals = ( terminalId : string ) => {
136+ const terminalIndexSorted = sortedTerminals . findIndex ( ( t ) => t . id === terminalId ) ;
137+ if ( terminalIndexSorted < 0 ) return ;
138+ const rightTerminals = sortedTerminals . slice ( terminalIndexSorted + 1 ) ;
139+ for ( let i = rightTerminals . length - 1 ; i >= 0 ; i -= 1 ) {
140+ const t = rightTerminals [ i ] ;
141+ if ( ! pinnedTerminalIds . includes ( t . id ) ) {
142+ onCloseTerminal ( t . id ) ;
143+ }
144+ }
145+ } ;
146+
147+ const handleCloseAllTerminals = ( ) => {
148+ for ( let i = terminals . length - 1 ; i >= 0 ; i -= 1 ) {
149+ const t = terminals [ i ] ;
150+ if ( ! pinnedTerminalIds . includes ( t . id ) ) {
151+ onCloseTerminal ( t . id ) ;
152+ }
153+ }
154+ } ;
155+
156+ const handleCloseAllAgents = ( ) => {
157+ agentSessions . forEach ( ( s ) => {
158+ if ( ! pinnedAgentIds . includes ( s . agentId ) ) {
159+ onCloseAgent ( s . agentId ) ;
160+ }
161+ } ) ;
162+ } ;
163+
44164 const handleTabsWheel = ( event : WheelEvent < HTMLDivElement > ) => {
45165 if ( event . deltaY === 0 ) return ;
46166 const tabsElement = event . currentTarget ;
@@ -62,24 +182,26 @@ export const Toolbar = ({
62182 case 'file' : {
63183 const file = files . find ( ( f ) => f . id === tabMenu . targetId ) ;
64184 if ( ! file ) return [ ] ;
65- const fileIndex = files . findIndex ( ( f ) => f . id === file . id ) ;
66- const hasRight = fileIndex >= 0 && files . length > fileIndex + 1 ;
185+ const isPinned = pinnedFileIds . includes ( file . id ) ;
186+ const fileIndexSorted = sortedFiles . findIndex ( ( f ) => f . id === file . id ) ;
187+ const hasRight = fileIndexSorted >= 0 && sortedFiles . length > fileIndexSorted + 1 ;
67188 return [
68- [ { key : 'copy-path' , label : 'Copy path' , onClick : ( ) => copyText ( file . id ) } ] ,
189+ [
190+ { key : 'copy-path' , label : 'Copy path' , onClick : ( ) => copyText ( file . id ) } ,
191+ { key : 'pin-file' , label : isPinned ? 'Unpin' : 'Pin' , onClick : ( ) => togglePinFile ( file . id ) } ,
192+ ] ,
69193 [
70194 { key : 'close' , label : 'Close' , onClick : ( ) => onCloseFile ( file . id ) } ,
71195 {
72196 key : 'close-right' ,
73197 label : 'Close right' ,
74198 disabled : ! hasRight ,
75- onClick : ( ) => {
76- files . slice ( fileIndex + 1 ) . forEach ( ( f ) => onCloseFile ( f . id ) ) ;
77- } ,
199+ onClick : ( ) => handleCloseRightFiles ( file . id ) ,
78200 } ,
79201 {
80202 key : 'close-all' ,
81203 label : 'Close all' ,
82- onClick : ( ) => files . forEach ( ( f ) => onCloseFile ( f . id ) ) ,
204+ onClick : handleCloseAllFiles ,
83205 } ,
84206 ] ,
85207 ] ;
@@ -88,37 +210,34 @@ export const Toolbar = ({
88210 const terminalIndex = terminals . findIndex ( ( t ) => t . id === tabMenu . targetId ) ;
89211 if ( terminalIndex < 0 ) return [ ] ;
90212 const terminal = terminals [ terminalIndex ] ;
91- const hasRight = terminals . length > terminalIndex + 1 ;
213+ const isPinned = pinnedTerminalIds . includes ( terminal . id ) ;
214+ const terminalIndexSorted = sortedTerminals . findIndex ( ( t ) => t . id === terminal . id ) ;
215+ const hasRight = sortedTerminals . length > terminalIndexSorted + 1 ;
92216 return [
93- [ { key : 'copy-terminal-name' , label : 'Copy terminal name' , onClick : ( ) => copyText ( terminal . name ) } ] ,
217+ [
218+ { key : 'copy-terminal-name' , label : 'Copy terminal name' , onClick : ( ) => copyText ( terminal . name ) } ,
219+ { key : 'pin-terminal' , label : isPinned ? 'Unpin' : 'Pin' , onClick : ( ) => togglePinTerminal ( terminal . id ) } ,
220+ ] ,
94221 [
95222 { key : 'close-terminal' , label : 'Close' , onClick : ( ) => onCloseTerminal ( terminal . id ) } ,
96223 {
97224 key : 'close-right-terminals' ,
98225 label : 'Close right' ,
99226 disabled : ! hasRight ,
100- // Close from the end so indices stay valid
101- onClick : ( ) => {
102- for ( let i = terminals . length - 1 ; i > terminalIndex ; i -= 1 ) {
103- onCloseTerminal ( terminals [ i ] . id ) ;
104- }
105- } ,
227+ onClick : ( ) => handleCloseRightTerminals ( terminal . id ) ,
106228 } ,
107229 {
108230 key : 'close-all-terminals' ,
109231 label : 'Close all' ,
110- onClick : ( ) => {
111- for ( let i = terminals . length - 1 ; i >= 0 ; i -= 1 ) {
112- onCloseTerminal ( terminals [ i ] . id ) ;
113- }
114- } ,
232+ onClick : handleCloseAllTerminals ,
115233 } ,
116234 ] ,
117235 ] ;
118236 }
119237 case 'agent' : {
120238 const agent = agentSessions . find ( ( s ) => s . agentId === tabMenu . targetId ) ;
121239 if ( ! agent ) return [ ] ;
240+ const isPinned = pinnedAgentIds . includes ( agent . agentId ) ;
122241 return [
123242 [
124243 { key : 'copy-agent-id' , label : 'Copy agent id' , onClick : ( ) => copyText ( agent . agentId ) } ,
@@ -127,13 +246,14 @@ export const Toolbar = ({
127246 label : 'Copy agent name' ,
128247 onClick : ( ) => copyText ( agent . agentName || agent . agentId ) ,
129248 } ,
249+ { key : 'pin-agent' , label : isPinned ? 'Unpin' : 'Pin' , onClick : ( ) => togglePinAgent ( agent . agentId ) } ,
130250 ] ,
131251 [
132252 { key : 'close-agent' , label : 'Close' , onClick : ( ) => onCloseAgent ( agent . agentId ) } ,
133253 {
134254 key : 'close-all-agents' ,
135255 label : 'Close all' ,
136- onClick : ( ) => agentSessions . forEach ( ( s ) => onCloseAgent ( s . agentId ) ) ,
256+ onClick : handleCloseAllAgents ,
137257 } ,
138258 ] ,
139259 ] ;
@@ -144,34 +264,40 @@ export const Toolbar = ({
144264 return (
145265 < div className = "toolbar" >
146266 < div className = "toolbar-tabs" onWheel = { handleTabsWheel } >
147- { files . map ( ( file ) => (
267+ { sortedFiles . map ( ( file ) => (
148268 < ToolbarTab
149269 key = { file . id }
150270 active = { activeFileId === file . id }
151271 label = { file . name }
152272 title = { file . id }
273+ pinned = { pinnedFileIds . includes ( file . id ) }
274+ onUnpin = { ( ) => togglePinFile ( file . id ) }
153275 onSelect = { ( ) => onSelectFile ( file . id ) }
154276 onClose = { ( ) => onCloseFile ( file . id ) }
155277 onContextMenu = { ( event ) => openMenu ( event , 'file' , file . id ) }
156278 />
157279 ) ) }
158- { terminals . map ( ( terminal ) => (
280+ { sortedTerminals . map ( ( terminal ) => (
159281 < ToolbarTab
160282 key = { `toolbar-terminal-${ terminal . id } ` }
161283 active = { activeTerminalId === terminal . id }
162284 label = { `term:${ terminal . name } ` }
163285 variant = "terminal"
286+ pinned = { pinnedTerminalIds . includes ( terminal . id ) }
287+ onUnpin = { ( ) => togglePinTerminal ( terminal . id ) }
164288 onSelect = { ( ) => onSelectTerminal ( terminal . id ) }
165289 onClose = { ( ) => onCloseTerminal ( terminal . id ) }
166290 onContextMenu = { ( event ) => openMenu ( event , 'terminal' , terminal . id ) }
167291 />
168292 ) ) }
169- { agentSessions . map ( ( session ) => (
293+ { sortedAgentSessions . map ( ( session ) => (
170294 < ToolbarTab
171295 key = { `toolbar-agent-${ session . agentId } ` }
172296 active = { activeAgentId === session . agentId }
173297 label = { session . agentName || session . agentId }
174298 variant = "agent"
299+ pinned = { pinnedAgentIds . includes ( session . agentId ) }
300+ onUnpin = { ( ) => togglePinAgent ( session . agentId ) }
175301 onSelect = { ( ) => onSelectAgent ( session . agentId ) }
176302 onClose = { ( ) => onCloseAgent ( session . agentId ) }
177303 onContextMenu = { ( event ) => openMenu ( event , 'agent' , session . agentId ) }
0 commit comments