@@ -267,8 +267,9 @@ interface SidebarThreadRowProps {
267267 renamingThreadId : ThreadId | null ;
268268 renamingTitle : string ;
269269 setRenamingTitle : ( title : string ) => void ;
270- renamingInputRef : MutableRefObject < HTMLInputElement | null > ;
271- renamingCommittedRef : MutableRefObject < boolean > ;
270+ onRenamingInputMount : ( element : HTMLInputElement | null ) => void ;
271+ hasRenameCommitted : ( ) => boolean ;
272+ markRenameCommitted : ( ) => void ;
272273 confirmingArchiveThreadId : ThreadId | null ;
273274 setConfirmingArchiveThreadId : Dispatch < SetStateAction < ThreadId | null > > ;
274275 confirmArchiveButtonRefs : MutableRefObject < Map < ThreadId , HTMLButtonElement > > ;
@@ -400,30 +401,24 @@ function SidebarThreadRow(props: SidebarThreadRowProps) {
400401 { threadStatus && < ThreadStatusLabel status = { threadStatus } /> }
401402 { props . renamingThreadId === thread . id ? (
402403 < input
403- ref = { ( element ) => {
404- if ( element && props . renamingInputRef . current !== element ) {
405- props . renamingInputRef . current = element ;
406- element . focus ( ) ;
407- element . select ( ) ;
408- }
409- } }
404+ ref = { props . onRenamingInputMount }
410405 className = "min-w-0 flex-1 truncate text-xs bg-transparent outline-none border border-ring rounded px-0.5"
411406 value = { props . renamingTitle }
412407 onChange = { ( event ) => props . setRenamingTitle ( event . target . value ) }
413408 onKeyDown = { ( event ) => {
414409 event . stopPropagation ( ) ;
415410 if ( event . key === "Enter" ) {
416411 event . preventDefault ( ) ;
417- props . renamingCommittedRef . current = true ;
412+ props . markRenameCommitted ( ) ;
418413 void props . commitRename ( thread . id , props . renamingTitle , thread . title ) ;
419414 } else if ( event . key === "Escape" ) {
420415 event . preventDefault ( ) ;
421- props . renamingCommittedRef . current = true ;
416+ props . markRenameCommitted ( ) ;
422417 props . cancelRename ( ) ;
423418 }
424419 } }
425420 onBlur = { ( ) => {
426- if ( ! props . renamingCommittedRef . current ) {
421+ if ( ! props . hasRenameCommitted ( ) ) {
427422 void props . commitRename ( thread . id , props . renamingTitle , thread . title ) ;
428423 }
429424 } }
@@ -718,13 +713,17 @@ export default function Sidebar() {
718713 const [ isAddingProject , setIsAddingProject ] = useState ( false ) ;
719714 const [ addProjectError , setAddProjectError ] = useState < string | null > ( null ) ;
720715 const addProjectInputRef = useRef < HTMLInputElement | null > ( null ) ;
716+ const [ renamingProjectId , setRenamingProjectId ] = useState < ProjectId | null > ( null ) ;
717+ const [ renamingProjectTitle , setRenamingProjectTitle ] = useState ( "" ) ;
721718 const [ renamingThreadId , setRenamingThreadId ] = useState < ThreadId | null > ( null ) ;
722719 const [ renamingTitle , setRenamingTitle ] = useState ( "" ) ;
723720 const [ confirmingArchiveThreadId , setConfirmingArchiveThreadId ] = useState < ThreadId | null > ( null ) ;
724721 const [ expandedThreadListsByProject , setExpandedThreadListsByProject ] = useState <
725722 ReadonlySet < ProjectId >
726723 > ( ( ) => new Set ( ) ) ;
727724 const { showThreadJumpHints, updateThreadJumpHintsVisibility } = useThreadJumpHintVisibility ( ) ;
725+ const projectRenamingCommittedRef = useRef ( false ) ;
726+ const projectRenamingInputRef = useRef < HTMLInputElement | null > ( null ) ;
728727 const renamingCommittedRef = useRef ( false ) ;
729728 const renamingInputRef = useRef < HTMLInputElement | null > ( null ) ;
730729 const confirmArchiveButtonRefs = useRef ( new Map < ThreadId , HTMLButtonElement > ( ) ) ;
@@ -937,6 +936,28 @@ export default function Sidebar() {
937936 renamingInputRef . current = null ;
938937 } , [ ] ) ;
939938
939+ const handleRenamingInputMount = useCallback ( ( element : HTMLInputElement | null ) => {
940+ if ( element && renamingInputRef . current !== element ) {
941+ renamingInputRef . current = element ;
942+ element . focus ( ) ;
943+ element . select ( ) ;
944+ return ;
945+ }
946+ if ( element === null && renamingInputRef . current !== null ) {
947+ renamingInputRef . current = null ;
948+ }
949+ } , [ ] ) ;
950+
951+ const hasRenameCommitted = useCallback ( ( ) => renamingCommittedRef . current , [ ] ) ;
952+ const markRenameCommitted = useCallback ( ( ) => {
953+ renamingCommittedRef . current = true ;
954+ } , [ ] ) ;
955+
956+ const cancelProjectRename = useCallback ( ( ) => {
957+ setRenamingProjectId ( null ) ;
958+ projectRenamingInputRef . current = null ;
959+ } , [ ] ) ;
960+
940961 const commitRename = useCallback (
941962 async ( threadId : ThreadId , newTitle : string , originalTitle : string ) => {
942963 const finishRename = ( ) => {
@@ -984,6 +1005,53 @@ export default function Sidebar() {
9841005 [ ] ,
9851006 ) ;
9861007
1008+ const commitProjectRename = useCallback (
1009+ async ( projectId : ProjectId , newTitle : string , originalTitle : string ) => {
1010+ const finishRename = ( ) => {
1011+ setRenamingProjectId ( ( current ) => {
1012+ if ( current !== projectId ) return current ;
1013+ projectRenamingInputRef . current = null ;
1014+ return null ;
1015+ } ) ;
1016+ } ;
1017+
1018+ const trimmed = newTitle . trim ( ) ;
1019+ if ( trimmed . length === 0 ) {
1020+ toastManager . add ( {
1021+ type : "warning" ,
1022+ title : "Project title cannot be empty" ,
1023+ } ) ;
1024+ finishRename ( ) ;
1025+ return ;
1026+ }
1027+ if ( trimmed === originalTitle ) {
1028+ finishRename ( ) ;
1029+ return ;
1030+ }
1031+ const api = readNativeApi ( ) ;
1032+ if ( ! api ) {
1033+ finishRename ( ) ;
1034+ return ;
1035+ }
1036+ try {
1037+ await api . orchestration . dispatchCommand ( {
1038+ type : "project.meta.update" ,
1039+ commandId : newCommandId ( ) ,
1040+ projectId,
1041+ title : trimmed ,
1042+ } ) ;
1043+ } catch ( error ) {
1044+ toastManager . add ( {
1045+ type : "error" ,
1046+ title : "Failed to rename project" ,
1047+ description : error instanceof Error ? error . message : "An error occurred." ,
1048+ } ) ;
1049+ }
1050+ finishRename ( ) ;
1051+ } ,
1052+ [ ] ,
1053+ ) ;
1054+
9871055 const { copyToClipboard : copyThreadIdToClipboard } = useCopyToClipboard < {
9881056 threadId : ThreadId ;
9891057 } > ( {
@@ -1040,6 +1108,8 @@ export default function Sidebar() {
10401108 ) ;
10411109
10421110 if ( clicked === "rename" ) {
1111+ setRenamingProjectId ( null ) ;
1112+ projectRenamingInputRef . current = null ;
10431113 setRenamingThreadId ( threadId ) ;
10441114 setRenamingTitle ( thread . title ) ;
10451115 renamingCommittedRef . current = false ;
@@ -1206,11 +1276,20 @@ export default function Sidebar() {
12061276
12071277 const clicked = await api . contextMenu . show (
12081278 [
1279+ { id : "rename" , label : "Rename project" } ,
12091280 { id : "copy-path" , label : "Copy Project Path" } ,
12101281 { id : "delete" , label : "Remove project" , destructive : true } ,
12111282 ] ,
12121283 position ,
12131284 ) ;
1285+ if ( clicked === "rename" ) {
1286+ setRenamingThreadId ( null ) ;
1287+ renamingInputRef . current = null ;
1288+ setRenamingProjectId ( projectId ) ;
1289+ setRenamingProjectTitle ( project . name ) ;
1290+ projectRenamingCommittedRef . current = false ;
1291+ return ;
1292+ }
12141293 if ( clicked === "copy-path" ) {
12151294 copyPathToClipboard ( project . cwd , { path : project . cwd } ) ;
12161295 return ;
@@ -1602,9 +1681,43 @@ export default function Sidebar() {
16021681 />
16031682 ) }
16041683 < ProjectFavicon cwd = { project . cwd } />
1605- < span className = "flex-1 truncate text-xs font-medium text-foreground/90" >
1606- { project . name }
1607- </ span >
1684+ { renamingProjectId === project . id ? (
1685+ < input
1686+ ref = { ( element ) => {
1687+ if ( element && projectRenamingInputRef . current !== element ) {
1688+ projectRenamingInputRef . current = element ;
1689+ element . focus ( ) ;
1690+ element . select ( ) ;
1691+ }
1692+ } }
1693+ className = "min-w-0 flex-1 truncate rounded border border-ring bg-transparent px-0.5 text-xs font-medium text-foreground/90 outline-none"
1694+ value = { renamingProjectTitle }
1695+ onChange = { ( event ) => setRenamingProjectTitle ( event . target . value ) }
1696+ onKeyDown = { ( event ) => {
1697+ event . stopPropagation ( ) ;
1698+ if ( event . key === "Enter" ) {
1699+ event . preventDefault ( ) ;
1700+ projectRenamingCommittedRef . current = true ;
1701+ void commitProjectRename ( project . id , renamingProjectTitle , project . name ) ;
1702+ } else if ( event . key === "Escape" ) {
1703+ event . preventDefault ( ) ;
1704+ projectRenamingCommittedRef . current = true ;
1705+ cancelProjectRename ( ) ;
1706+ }
1707+ } }
1708+ onBlur = { ( ) => {
1709+ if ( ! projectRenamingCommittedRef . current ) {
1710+ void commitProjectRename ( project . id , renamingProjectTitle , project . name ) ;
1711+ }
1712+ } }
1713+ onClick = { ( event ) => event . stopPropagation ( ) }
1714+ onPointerDown = { ( event ) => event . stopPropagation ( ) }
1715+ />
1716+ ) : (
1717+ < span className = "flex-1 truncate text-xs font-medium text-foreground/90" >
1718+ { project . name }
1719+ </ span >
1720+ ) }
16081721 </ SidebarMenuButton >
16091722 < Tooltip >
16101723 < TooltipTrigger
@@ -1693,8 +1806,9 @@ export default function Sidebar() {
16931806 renamingThreadId = { renamingThreadId }
16941807 renamingTitle = { renamingTitle }
16951808 setRenamingTitle = { setRenamingTitle }
1696- renamingInputRef = { renamingInputRef }
1697- renamingCommittedRef = { renamingCommittedRef }
1809+ onRenamingInputMount = { handleRenamingInputMount }
1810+ hasRenameCommitted = { hasRenameCommitted }
1811+ markRenameCommitted = { markRenameCommitted }
16981812 confirmingArchiveThreadId = { confirmingArchiveThreadId }
16991813 setConfirmingArchiveThreadId = { setConfirmingArchiveThreadId }
17001814 confirmArchiveButtonRefs = { confirmArchiveButtonRefs }
0 commit comments