1- import React , { useState , useEffect , useCallback , useRef } from "react" ;
1+ import React , { useState , useEffect , useCallback , useRef , useMemo } from "react" ;
22import { createPortal } from "react-dom" ;
33import styled from "@emotion/styled" ;
44import { css } from "@emotion/react" ;
5- import type { ProjectConfig } from "@/config" ;
5+ import type { ProjectConfig , Workspace } from "@/config" ;
66import type { WorkspaceMetadata } from "@/types/workspace" ;
77import { useGitStatus } from "@/contexts/GitStatusContext" ;
88import { usePersistedState } from "@/hooks/usePersistedState" ;
@@ -585,6 +585,7 @@ interface ProjectSidebarProps {
585585 onToggleCollapsed : ( ) => void ;
586586 onGetSecrets : ( projectPath : string ) => Promise < Secret [ ] > ;
587587 onUpdateSecrets : ( projectPath : string , secrets : Secret [ ] ) => Promise < void > ;
588+ workspaceRecency : Record < string , number > ;
588589}
589590
590591const ProjectSidebar : React . FC < ProjectSidebarProps > = ( {
@@ -604,10 +605,33 @@ const ProjectSidebar: React.FC<ProjectSidebarProps> = ({
604605 onToggleCollapsed,
605606 onGetSecrets,
606607 onUpdateSecrets,
608+ workspaceRecency,
607609} ) => {
608610 // Subscribe to git status updates (causes this component to re-render every 10s)
609611 const gitStatus = useGitStatus ( ) ;
610612
613+ // Sort workspaces by last user message (most recent first)
614+ // workspaceRecency only updates when timestamps actually change (stable reference optimization)
615+ const sortedWorkspacesByProject = useMemo ( ( ) => {
616+ const result = new Map < string , Workspace [ ] > ( ) ;
617+ for ( const [ projectPath , config ] of projects ) {
618+ result . set (
619+ projectPath ,
620+ config . workspaces . slice ( ) . sort ( ( a , b ) => {
621+ const aMeta = workspaceMetadata . get ( a . path ) ;
622+ const bMeta = workspaceMetadata . get ( b . path ) ;
623+ if ( ! aMeta || ! bMeta ) return 0 ;
624+
625+ // Get timestamp of most recent user message (0 if never used)
626+ const aTimestamp = workspaceRecency [ aMeta . id ] ?? 0 ;
627+ const bTimestamp = workspaceRecency [ bMeta . id ] ?? 0 ;
628+ return bTimestamp - aTimestamp ;
629+ } )
630+ ) ;
631+ }
632+ return result ;
633+ } , [ projects , workspaceMetadata , workspaceRecency ] ) ;
634+
611635 // Store as array in localStorage, convert to Set for usage
612636 const [ expandedProjectsArray , setExpandedProjectsArray ] = usePersistedState < string [ ] > (
613637 "expandedProjects" ,
@@ -964,109 +988,112 @@ const ProjectSidebar: React.FC<ProjectSidebarProps> = ({
964988 ` (${ formatKeybind ( KEYBINDS . NEW_WORKSPACE ) } )` }
965989 </ AddWorkspaceBtn >
966990 </ WorkspaceHeader >
967- { config . workspaces . map ( ( workspace ) => {
968- const metadata = workspaceMetadata . get ( workspace . path ) ;
969- if ( ! metadata ) return null ;
970-
971- const workspaceId = metadata . id ;
972- const displayName = getWorkspaceDisplayName ( workspace . path ) ;
973- const workspaceState = getWorkspaceState ( workspaceId ) ;
974- const isStreaming = workspaceState . canInterrupt ;
975- // const streamingModel = workspaceState.currentModel; // Unused
976- const isUnread = unreadStatus . get ( workspaceId ) ?? false ;
977- const isEditing = editingWorkspaceId === workspaceId ;
978- const isSelected = selectedWorkspace ?. workspacePath === workspace . path ;
979-
980- return (
981- < React . Fragment key = { workspace . path } >
982- < WorkspaceItem
983- selected = { isSelected }
984- onClick = { ( ) =>
985- onSelectWorkspace ( {
986- projectPath,
987- projectName,
988- workspacePath : workspace . path ,
989- workspaceId,
990- } )
991- }
992- onKeyDown = { ( e ) => {
993- if ( e . key === "Enter" || e . key === " " ) {
994- e . preventDefault ( ) ;
991+ { ( sortedWorkspacesByProject . get ( projectPath ) ?? config . workspaces ) . map (
992+ ( workspace ) => {
993+ const metadata = workspaceMetadata . get ( workspace . path ) ;
994+ if ( ! metadata ) return null ;
995+
996+ const workspaceId = metadata . id ;
997+ const displayName = getWorkspaceDisplayName ( workspace . path ) ;
998+ const workspaceState = getWorkspaceState ( workspaceId ) ;
999+ const isStreaming = workspaceState . canInterrupt ;
1000+ // const streamingModel = workspaceState.currentModel; // Unused
1001+ const isUnread = unreadStatus . get ( workspaceId ) ?? false ;
1002+ const isEditing = editingWorkspaceId === workspaceId ;
1003+ const isSelected =
1004+ selectedWorkspace ?. workspacePath === workspace . path ;
1005+
1006+ return (
1007+ < React . Fragment key = { workspace . path } >
1008+ < WorkspaceItem
1009+ selected = { isSelected }
1010+ onClick = { ( ) =>
9951011 onSelectWorkspace ( {
9961012 projectPath,
9971013 projectName,
9981014 workspacePath : workspace . path ,
9991015 workspaceId,
1000- } ) ;
1016+ } )
10011017 }
1002- } }
1003- role = "button"
1004- tabIndex = { 0 }
1005- aria-current = { isSelected ? "true" : undefined }
1006- data-workspace-path = { workspace . path }
1007- data-workspace-id = { workspaceId }
1008- >
1009- < TooltipWrapper inline >
1010- < WorkspaceRemoveBtn
1011- onClick = { ( e ) => {
1012- e . stopPropagation ( ) ;
1013- void handleRemoveWorkspace ( workspaceId , e . currentTarget ) ;
1014- } }
1015- aria-label = { `Remove workspace ${ displayName } ` }
1016- data-workspace-id = { workspaceId }
1017- >
1018- ×
1019- </ WorkspaceRemoveBtn >
1020- < Tooltip className = "tooltip" align = "right" >
1021- Remove workspace
1022- </ Tooltip >
1023- </ TooltipWrapper >
1024- < GitStatusIndicator
1025- gitStatus = { gitStatus . get ( metadata . id ) ?? null }
1026- workspaceId = { workspaceId }
1027- tooltipPosition = "right"
1028- />
1029- { isEditing ? (
1030- < WorkspaceNameInput
1031- value = { editingName }
1032- onChange = { ( e ) => setEditingName ( e . target . value ) }
1033- onKeyDown = { ( e ) => handleRenameKeyDown ( e , workspaceId ) }
1034- onBlur = { ( ) => void confirmRename ( workspaceId ) }
1035- autoFocus
1036- onClick = { ( e ) => e . stopPropagation ( ) }
1037- aria-label = { `Rename workspace ${ displayName } ` }
1038- data-workspace-id = { workspaceId }
1018+ onKeyDown = { ( e ) => {
1019+ if ( e . key === "Enter" || e . key === " " ) {
1020+ e . preventDefault ( ) ;
1021+ onSelectWorkspace ( {
1022+ projectPath,
1023+ projectName,
1024+ workspacePath : workspace . path ,
1025+ workspaceId,
1026+ } ) ;
1027+ }
1028+ } }
1029+ role = "button"
1030+ tabIndex = { 0 }
1031+ aria-current = { isSelected ? "true" : undefined }
1032+ data-workspace-path = { workspace . path }
1033+ data-workspace-id = { workspaceId }
1034+ >
1035+ < TooltipWrapper inline >
1036+ < WorkspaceRemoveBtn
1037+ onClick = { ( e ) => {
1038+ e . stopPropagation ( ) ;
1039+ void handleRemoveWorkspace ( workspaceId , e . currentTarget ) ;
1040+ } }
1041+ aria-label = { `Remove workspace ${ displayName } ` }
1042+ data-workspace-id = { workspaceId }
1043+ >
1044+ ×
1045+ </ WorkspaceRemoveBtn >
1046+ < Tooltip className = "tooltip" align = "right" >
1047+ Remove workspace
1048+ </ Tooltip >
1049+ </ TooltipWrapper >
1050+ < GitStatusIndicator
1051+ gitStatus = { gitStatus . get ( metadata . id ) ?? null }
1052+ workspaceId = { workspaceId }
1053+ tooltipPosition = "right"
10391054 />
1040- ) : (
1041- < WorkspaceName
1042- onDoubleClick = { ( e ) => {
1043- e . stopPropagation ( ) ;
1044- startRenaming ( workspaceId , displayName ) ;
1045- } }
1046- title = "Double-click to rename"
1047- >
1048- { displayName }
1049- </ WorkspaceName >
1055+ { isEditing ? (
1056+ < WorkspaceNameInput
1057+ value = { editingName }
1058+ onChange = { ( e ) => setEditingName ( e . target . value ) }
1059+ onKeyDown = { ( e ) => handleRenameKeyDown ( e , workspaceId ) }
1060+ onBlur = { ( ) => void confirmRename ( workspaceId ) }
1061+ autoFocus
1062+ onClick = { ( e ) => e . stopPropagation ( ) }
1063+ aria-label = { `Rename workspace ${ displayName } ` }
1064+ data-workspace-id = { workspaceId }
1065+ />
1066+ ) : (
1067+ < WorkspaceName
1068+ onDoubleClick = { ( e ) => {
1069+ e . stopPropagation ( ) ;
1070+ startRenaming ( workspaceId , displayName ) ;
1071+ } }
1072+ title = "Double-click to rename"
1073+ >
1074+ { displayName }
1075+ </ WorkspaceName >
1076+ ) }
1077+ < WorkspaceStatusIndicator
1078+ streaming = { isStreaming }
1079+ unread = { isUnread }
1080+ onClick = { ( ) => _onToggleUnread ( workspaceId ) }
1081+ title = {
1082+ isStreaming
1083+ ? "Assistant is responding"
1084+ : isUnread
1085+ ? "Unread messages"
1086+ : "Idle"
1087+ }
1088+ />
1089+ </ WorkspaceItem >
1090+ { renameError && editingWorkspaceId === workspaceId && (
1091+ < WorkspaceErrorContainer > { renameError } </ WorkspaceErrorContainer >
10501092 ) }
1051- < WorkspaceStatusIndicator
1052- streaming = { isStreaming }
1053- unread = { isUnread }
1054- onClick = { ( ) => _onToggleUnread ( workspaceId ) }
1055- title = {
1056- isStreaming
1057- ? "Assistant is responding"
1058- : isUnread
1059- ? "Unread messages"
1060- : "Idle"
1061- }
1062- />
1063- </ WorkspaceItem >
1064- { renameError && editingWorkspaceId === workspaceId && (
1065- < WorkspaceErrorContainer > { renameError } </ WorkspaceErrorContainer >
1066- ) }
1067- </ React . Fragment >
1068- ) ;
1069- } ) }
1093+ </ React . Fragment >
1094+ ) ;
1095+ }
1096+ ) }
10701097 </ WorkspacesContainer >
10711098 ) }
10721099 </ ProjectGroup >
0 commit comments