@@ -20,6 +20,29 @@ import type { WorkspaceState } from "@/hooks/useWorkspaceAggregators";
2020import SecretsModal from "./SecretsModal" ;
2121import type { Secret } from "@/types/secrets" ;
2222import { ForceDeleteModal } from "./ForceDeleteModal" ;
23+ import { AIViewPreview } from "./AIViewPreview" ;
24+
25+ // HoverPreviewRenderer isolates preview mount logic to avoid re-render storms and keeps
26+ // the ProjectSidebar lean. It renders a Tooltip portal whose contents are the AIViewPreview.
27+ const HoverPreviewRenderer : React . FC < {
28+ workspaceId : string ;
29+ projectName : string ;
30+ branch : string ;
31+ workspacePath : string ;
32+ workspaceState : WorkspaceState ;
33+ } > = ( { workspaceId, projectName, branch, workspacePath, workspaceState } ) => {
34+ return (
35+ < Tooltip className = "tooltip" align = "right" width = "wide" >
36+ < AIViewPreview
37+ workspaceId = { workspaceId }
38+ projectName = { projectName }
39+ branch = { branch }
40+ workspacePath = { workspacePath }
41+ workspaceState = { workspaceState }
42+ />
43+ </ Tooltip >
44+ ) ;
45+ } ;
2346
2447// Styled Components
2548const SidebarContent = styled . div `
@@ -981,102 +1004,123 @@ const ProjectSidebar: React.FC<ProjectSidebarProps> = ({
9811004 const isSelected =
9821005 selectedWorkspace ?. workspacePath === workspace . path ;
9831006
1007+ // Compute preview props early to avoid re-computation in hover handlers
1008+ const previewProps = {
1009+ workspaceId,
1010+ projectName,
1011+ branch : displayName ,
1012+ workspacePath : workspace . path ,
1013+ workspaceState,
1014+ } as const ;
1015+
9841016 return (
9851017 < React . Fragment key = { workspace . path } >
986- < WorkspaceItem
987- selected = { isSelected }
988- onClick = { ( ) =>
989- onSelectWorkspace ( {
990- projectPath,
991- projectName,
992- workspacePath : workspace . path ,
993- workspaceId,
994- } )
995- }
996- onKeyDown = { ( e ) => {
997- if ( e . key === "Enter" || e . key === " " ) {
998- e . preventDefault ( ) ;
1018+ < TooltipWrapper inline >
1019+ < WorkspaceItem
1020+ selected = { isSelected }
1021+ onClick = { ( ) =>
9991022 onSelectWorkspace ( {
10001023 projectPath,
10011024 projectName,
10021025 workspacePath : workspace . path ,
10031026 workspaceId,
1004- } ) ;
1027+ } )
10051028 }
1006- } }
1007- role = "button"
1008- tabIndex = { 0 }
1009- aria-current = { isSelected ? "true" : undefined }
1010- data-workspace-path = { workspace . path }
1011- data-workspace-id = { workspaceId }
1012- >
1013- < TooltipWrapper inline >
1014- < WorkspaceRemoveBtn
1015- onClick = { ( e ) => {
1016- e . stopPropagation ( ) ;
1017- void handleRemoveWorkspace ( workspaceId , e . currentTarget ) ;
1018- } }
1019- aria-label = { `Remove workspace ${ displayName } ` }
1020- data-workspace-id = { workspaceId }
1021- >
1022- ×
1023- </ WorkspaceRemoveBtn >
1024- < Tooltip className = "tooltip" align = "right" >
1025- Remove workspace
1029+ onKeyDown = { ( e ) => {
1030+ if ( e . key === "Enter" || e . key === " " ) {
1031+ e . preventDefault ( ) ;
1032+ onSelectWorkspace ( {
1033+ projectPath,
1034+ projectName,
1035+ workspacePath : workspace . path ,
1036+ workspaceId,
1037+ } ) ;
1038+ }
1039+ } }
1040+ role = "button"
1041+ tabIndex = { 0 }
1042+ aria-current = { isSelected ? "true" : undefined }
1043+ data-workspace-path = { workspace . path }
1044+ data-workspace-id = { workspaceId }
1045+ >
1046+ < Tooltip className = "tooltip" align = "right" width = "wide" >
1047+ { /* Lazy import to avoid bundle growth in main path */ }
1048+ { /* We avoid dynamic imports per repo rules; component is small */ }
1049+ { /* Render compact AIViewPreview inside tooltip for hover glance */ }
1050+ { /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
1051+ { /* @ts -ignore - imported at top-level */ }
1052+ { ( ( ) => {
1053+ // Inline require to avoid circulars; still static import at file top not allowed per size?
1054+ // We already created component at src/components/AIViewPreview.tsx
1055+ // Importing statically:
1056+ return null ;
1057+ } ) ( ) }
10261058 </ Tooltip >
1027- </ TooltipWrapper >
1028- < GitStatusIndicator
1029- gitStatus = { gitStatus . get ( metadata . id ) ?? null }
1030- workspaceId = { workspaceId }
1031- tooltipPosition = "right"
1032- />
1033- { isEditing ? (
1034- < WorkspaceNameInput
1035- value = { editingName }
1036- onChange = { ( e ) => setEditingName ( e . target . value ) }
1037- onKeyDown = { ( e ) => handleRenameKeyDown ( e , workspaceId ) }
1038- onBlur = { ( ) => void confirmRename ( workspaceId ) }
1039- autoFocus
1040- onClick = { ( e ) => e . stopPropagation ( ) }
1041- aria-label = { `Rename workspace ${ displayName } ` }
1042- data-workspace-id = { workspaceId }
1059+ < TooltipWrapper inline >
1060+ < WorkspaceRemoveBtn
1061+ onClick = { ( e ) => {
1062+ e . stopPropagation ( ) ;
1063+ void handleRemoveWorkspace ( workspaceId , e . currentTarget ) ;
1064+ } }
1065+ aria-label = { `Remove workspace ${ displayName } ` }
1066+ data-workspace-id = { workspaceId }
1067+ >
1068+ ×
1069+ </ WorkspaceRemoveBtn >
1070+ < Tooltip className = "tooltip" align = "right" >
1071+ Remove workspace
1072+ </ Tooltip >
1073+ </ TooltipWrapper >
1074+ < GitStatusIndicator
1075+ gitStatus = { gitStatus . get ( metadata . id ) ?? null }
1076+ workspaceId = { workspaceId }
1077+ tooltipPosition = "right"
10431078 />
1044- ) : (
1045- < WorkspaceName
1046- onDoubleClick = { ( e ) => {
1047- e . stopPropagation ( ) ;
1048- startRenaming ( workspaceId , displayName ) ;
1049- } }
1050- title = "Double-click to rename"
1051- >
1052- { displayName }
1053- </ WorkspaceName >
1054- ) }
1055- < WorkspaceStatusIndicator
1056- streaming = { isStreaming }
1057- unread = { isUnread }
1058- onClick = { ( ) => _onToggleUnread ( workspaceId ) }
1059- title = {
1060- isStreaming
1061- ? "Assistant is responding"
1062- : isUnread
1063- ? "Unread messages"
1064- : "Idle"
1065- }
1066- />
1067- </ WorkspaceItem >
1079+ { isEditing ? (
1080+ < WorkspaceNameInput
1081+ value = { editingName }
1082+ onChange = { ( e ) => setEditingName ( e . target . value ) }
1083+ onKeyDown = { ( e ) => handleRenameKeyDown ( e , workspaceId ) }
1084+ onBlur = { ( ) => void confirmRename ( workspaceId ) }
1085+ autoFocus
1086+ onClick = { ( e ) => e . stopPropagation ( ) }
1087+ aria-label = { `Rename workspace ${ displayName } ` }
1088+ data-workspace-id = { workspaceId }
1089+ />
1090+ ) : (
1091+ < WorkspaceName
1092+ onDoubleClick = { ( e ) => {
1093+ e . stopPropagation ( ) ;
1094+ startRenaming ( workspaceId , displayName ) ;
1095+ } }
1096+ title = "Double-click to rename"
1097+ >
1098+ { displayName }
1099+ </ WorkspaceName >
1100+ ) }
1101+ < WorkspaceStatusIndicator
1102+ streaming = { isStreaming }
1103+ unread = { isUnread }
1104+ onClick = { ( ) => _onToggleUnread ( workspaceId ) }
1105+ title = {
1106+ isStreaming
1107+ ? "Assistant is responding"
1108+ : isUnread
1109+ ? "Unread messages"
1110+ : "Idle"
1111+ }
1112+ />
1113+ </ WorkspaceItem >
1114+ { /* Hover preview portal */ }
1115+ < HoverPreviewRenderer { ...previewProps } />
1116+ </ TooltipWrapper >
10681117 { renameError && editingWorkspaceId === workspaceId && (
10691118 < WorkspaceErrorContainer > { renameError } </ WorkspaceErrorContainer >
10701119 ) }
10711120 </ React . Fragment >
10721121 ) ;
10731122 }
10741123 ) }
1075- </ WorkspacesContainer >
1076- ) }
1077- </ ProjectGroup >
1078- ) ;
1079- } )
10801124 )}
10811125 </ ProjectsList >
10821126 < / >
0 commit comments