@@ -224,6 +224,11 @@ function FolderNode({
224224 onSelect,
225225 folderDrop,
226226 dropPath,
227+ onContext,
228+ renaming,
229+ onRename,
230+ onCommitRename,
231+ onCancelRename,
227232} : {
228233 path : string ;
229234 name : string ;
@@ -234,6 +239,13 @@ function FolderNode({
234239 folderPath : string ,
235240 ) => Pick < React . HTMLAttributes < HTMLDivElement > , 'onDragEnter' | 'onDragOver' | 'onDragLeave' | 'onDrop' > ;
236241 dropPath ?: string | null ;
242+ /** Right-click a folder row → the shared folder context menu (Open/Rename/Delete…). */
243+ onContext ?: ( path : string , name : string , ev : React . MouseEvent ) => void ;
244+ /** The path currently being inline-renamed (shows the editor in that row). */
245+ renaming ?: string | null ;
246+ onRename ?: ( path : string ) => void ; // F2 → start renaming this folder
247+ onCommitRename ?: ( path : string , value : string ) => void ;
248+ onCancelRename ?: ( ) => void ;
237249} ) {
238250 const [ open , setOpen ] = useState ( depth === 0 ) ;
239251 const children = useDir ( open ? path : null ) . entries ;
@@ -248,6 +260,14 @@ function FolderNode({
248260 e . preventDefault ( ) ;
249261 onSelect ( path ) ;
250262 break ;
263+ case 'F2' :
264+ // Rename in the tree, matching the grid — the root (depth 0 = project) has
265+ // no real folder path to rename.
266+ if ( depth > 0 && onRename ) {
267+ e . preventDefault ( ) ;
268+ onRename ( path ) ;
269+ }
270+ break ;
251271 case 'ArrowRight' :
252272 e . preventDefault ( ) ;
253273 e . stopPropagation ( ) ;
@@ -284,6 +304,7 @@ function FolderNode({
284304 tabIndex = { 0 }
285305 onKeyDown = { onRowKey }
286306 onClick = { ( ) => onSelect ( path ) }
307+ onContextMenu = { depth > 0 && onContext ? ( ev ) => onContext ( path , name , ev ) : undefined }
287308 { ...( folderDrop ? folderDrop ( path ) : null ) }
288309 >
289310 < span
@@ -298,10 +319,15 @@ function FolderNode({
298319 < span className = "ti" >
299320 < AssetIcon type = "folder" size = { 14 } />
300321 </ span >
301- < span className = "tn" > { name } </ span >
322+ { renaming === path && onCommitRename && onCancelRename ? (
323+ < RenameInput name = { name } onCommit = { ( v ) => onCommitRename ( path , v ) } onCancel = { onCancelRename } />
324+ ) : (
325+ < span className = "tn" > { name } </ span >
326+ ) }
302327 </ div >
303328 { open && subdirs . map ( ( d ) => (
304- < FolderNode key = { d . name } path = { join ( path , d . name ) } name = { d . name } depth = { depth + 1 } cwd = { cwd } onSelect = { onSelect } folderDrop = { folderDrop } dropPath = { dropPath } />
329+ < FolderNode key = { d . name } path = { join ( path , d . name ) } name = { d . name } depth = { depth + 1 } cwd = { cwd } onSelect = { onSelect } folderDrop = { folderDrop } dropPath = { dropPath }
330+ onContext = { onContext } renaming = { renaming } onRename = { onRename } onCommitRename = { onCommitRename } onCancelRename = { onCancelRename } />
305331 ) ) }
306332 </ >
307333 ) ;
@@ -312,8 +338,16 @@ export function ContentBrowser() {
312338 const { cwd, go, back, forward, up, reset, canBack, canForward, canUp } = useNav ( ) ;
313339 const [ query , setQuery ] = useState ( '' ) ;
314340 // A right-click menu: on an item (target set) or on empty space (target null).
315- const [ ctx , setCtx ] = useState < { x : number ; y : number ; target : { path : string ; entry : DirEntry } | null } | null > ( null ) ;
341+ const [ ctx , setCtx ] = useState < { x : number ; y : number ; target : { path : string ; entry : DirEntry ; inTree ?: boolean } | null } | null > ( null ) ;
316342 const [ renaming , setRenaming ] = useState < string | null > ( null ) ;
343+ // A folder can appear in BOTH the tree and the grid (cwd = its parent). Track
344+ // which surface owns the rename so only ONE editor mounts — two RenameInputs on
345+ // the same path fight for focus and immediately commit each other closed.
346+ const [ renameInTree , setRenameInTree ] = useState ( false ) ;
347+ const startRename = ( path : string , inTree = false ) => {
348+ setRenameInTree ( inTree ) ;
349+ setRenaming ( path ) ;
350+ } ;
317351 // The tile being dragged dims so the source of the move reads at a glance.
318352 const [ dragPath , setDragPath ] = useState < string | null > ( null ) ;
319353 const [ filters , setFilters ] = useState < Set < AssetType > > ( new Set ( ) ) ;
@@ -619,7 +653,7 @@ export function ContentBrowser() {
619653 case 'F2' : {
620654 if ( idx >= 0 && selected ) {
621655 e . preventDefault ( ) ;
622- setRenaming ( selected ) ;
656+ startRename ( selected ) ;
623657 }
624658 break ;
625659 }
@@ -670,7 +704,7 @@ export function ContentBrowser() {
670704 await window . estella . fs . mkdir ( path ) ;
671705 refreshFs ( ) ;
672706 selectAsset ( path ) ;
673- setRenaming ( path ) ; // drop straight into rename, like UE5
707+ startRename ( path ) ; // drop straight into rename, like UE5
674708 } catch ( e ) {
675709 Toasts . push ( t ( 'cb.newFolderFailed' , { error : errMsg ( e ) } ) , 'error' ) ;
676710 }
@@ -681,7 +715,7 @@ export function ContentBrowser() {
681715 const path = await ProjectStore . createSceneFile ( cwd ) ;
682716 refreshFs ( ) ;
683717 selectAsset ( path ) ;
684- setRenaming ( path ) ; // drop into rename, like New Folder
718+ startRename ( path ) ; // drop into rename, like New Folder
685719 } catch ( e ) {
686720 Toasts . push ( t ( 'cb.newSceneFailed' , { error : errMsg ( e ) } ) , 'error' ) ;
687721 }
@@ -692,7 +726,7 @@ export function ContentBrowser() {
692726 const path = await ProjectStore . createInputMapFile ( cwd ) ;
693727 refreshFs ( ) ;
694728 selectAsset ( path ) ; // unified inspector opens the input-map editor
695- setRenaming ( path ) ;
729+ startRename ( path ) ;
696730 } catch ( e ) {
697731 Toasts . push ( t ( 'cb.newInputMapFailed' , { error : errMsg ( e ) } ) , 'error' ) ;
698732 }
@@ -703,7 +737,7 @@ export function ContentBrowser() {
703737 const path = await ProjectStore . createLocaleTableFile ( cwd ) ;
704738 refreshFs ( ) ;
705739 selectAsset ( path ) ;
706- setRenaming ( path ) ;
740+ startRename ( path ) ;
707741 } catch ( e ) {
708742 Toasts . push ( t ( 'cb.newLocaleTableFailed' , { error : errMsg ( e ) } ) , 'error' ) ;
709743 }
@@ -969,7 +1003,7 @@ export function ContentBrowser() {
9691003 ...( isMaterial
9701004 ? [ { label : t ( 'cb.menuCreateMaterialInstance' ) , onClick : ( ) => void createMaterialInstance ( path ) } ]
9711005 : [ ] ) ,
972- { label : t ( 'ui.rename' ) , onClick : ( ) => setRenaming ( path ) } ,
1006+ { label : t ( 'ui.rename' ) , onClick : ( ) => startRename ( path , ! ! ctx ?. target ?. inTree ) } ,
9731007 { label : t ( 'cb.menuDuplicate' ) , onClick : ( ) => void duplicate ( path ) } ,
9741008 { sep : true } ,
9751009 { label : t ( 'cb.menuCopyPath' ) , onClick : ( ) => copy ( path , t ( 'cb.copiedPath' ) ) } ,
@@ -999,7 +1033,14 @@ export function ContentBrowser() {
9991033 </ div >
10001034 < div className = "cb-src-body" role = "tree" aria-label = { t ( 'cb.folders' ) } >
10011035 < div className = "cb-sec" > { t ( 'cb.folders' ) } </ div >
1002- < FolderNode path = "" name = { project . name } depth = { 0 } cwd = { cwd } onSelect = { go } folderDrop = { folderDrop } dropPath = { dropFolder } />
1036+ < FolderNode path = "" name = { project . name } depth = { 0 } cwd = { cwd } onSelect = { go } folderDrop = { folderDrop } dropPath = { dropFolder }
1037+ onContext = { ( p , n , ev ) => {
1038+ ev . preventDefault ( ) ;
1039+ ev . stopPropagation ( ) ;
1040+ setCtx ( { x : ev . clientX , y : ev . clientY , target : { path : p , entry : { name : n , isDir : true } , inTree : true } } ) ;
1041+ } }
1042+ renaming = { renameInTree ? renaming : null } onRename = { ( p ) => startRename ( p , true ) }
1043+ onCommitRename = { ( p , v ) => void commitRename ( p , v ) } onCancelRename = { ( ) => setRenaming ( null ) } />
10031044 </ div >
10041045 </ div >
10051046
@@ -1177,7 +1218,7 @@ export function ContentBrowser() {
11771218 className = "nm"
11781219 style = { it . isDir ? undefined : ( { [ '--tc' as string ] : assetTint ( type ) } as React . CSSProperties ) }
11791220 >
1180- { renaming === path ? (
1221+ { renaming === path && ! renameInTree ? (
11811222 < RenameInput
11821223 name = { it . name }
11831224 onCommit = { ( v ) => void commitRename ( path , v ) }
@@ -1221,7 +1262,7 @@ export function ContentBrowser() {
12211262 >
12221263 < span className = "ln" >
12231264 < AssetIcon type = { type } size = { 15 } />
1224- { renaming === path ? (
1265+ { renaming === path && ! renameInTree ? (
12251266 < RenameInput
12261267 name = { it . name }
12271268 onCommit = { ( v ) => void commitRename ( path , v ) }
0 commit comments