diff --git a/ui/desktop/src/components/SpaceBreadcrumb.tsx b/ui/desktop/src/components/SpaceBreadcrumb.tsx index 9cb66b9863..b74d751ad8 100644 --- a/ui/desktop/src/components/SpaceBreadcrumb.tsx +++ b/ui/desktop/src/components/SpaceBreadcrumb.tsx @@ -1,11 +1,13 @@ -import React, { useState, useEffect } from 'react'; -import { ChevronRight, Hash } from 'lucide-react'; +import React, { useState, useEffect, useRef, useCallback } from 'react'; +import { ChevronRight, Hash, ChevronDown, Layers } from 'lucide-react'; import { matrixService } from '../services/MatrixService'; -import { motion } from 'framer-motion'; +import { motion, AnimatePresence } from 'framer-motion'; interface SpaceBreadcrumbProps { roomId: string; className?: string; + onSpaceClick?: (spaceId: string) => void; + onRoomClick?: (roomId: string) => void; } interface BreadcrumbData { @@ -15,14 +17,27 @@ interface BreadcrumbData { roomId: string; } -export const SpaceBreadcrumb: React.FC = ({ roomId, className = '' }) => { +export const SpaceBreadcrumb: React.FC = ({ + roomId, + className = '', + onSpaceClick, + onRoomClick +}) => { const [breadcrumb, setBreadcrumb] = useState(null); + const [showSpaceDropdown, setShowSpaceDropdown] = useState(false); + const [showRoomDropdown, setShowRoomDropdown] = useState(false); + const [allSpaces, setAllSpaces] = useState>([]); + const [spaceRooms, setSpaceRooms] = useState>([]); + + const spaceDropdownRef = useRef(null); + const roomDropdownRef = useRef(null); useEffect(() => { const loadBreadcrumb = () => { try { // Get all Spaces const spaces = matrixService.getSpaces(); + setAllSpaces(spaces.map(s => ({ roomId: s.roomId, name: s.name || 'Unnamed Space' }))); // Find which Space contains this room for (const space of spaces) { @@ -37,6 +52,12 @@ export const SpaceBreadcrumb: React.FC = ({ roomId, classN roomName: childRoom.name || 'Unnamed Room', roomId: roomId, }); + + // Load all rooms in this Space + setSpaceRooms(children.map(c => ({ + roomId: c.roomId, + name: c.name || 'Unnamed Room' + }))); return; } } @@ -67,6 +88,31 @@ export const SpaceBreadcrumb: React.FC = ({ roomId, classN }; }, [roomId]); + // Close dropdowns when clicking outside + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (spaceDropdownRef.current && !spaceDropdownRef.current.contains(event.target as Node)) { + setShowSpaceDropdown(false); + } + if (roomDropdownRef.current && !roomDropdownRef.current.contains(event.target as Node)) { + setShowRoomDropdown(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleSpaceSelect = useCallback((spaceId: string) => { + setShowSpaceDropdown(false); + onSpaceClick?.(spaceId); + }, [onSpaceClick]); + + const handleRoomSelect = useCallback((selectedRoomId: string) => { + setShowRoomDropdown(false); + onRoomClick?.(selectedRoomId); + }, [onRoomClick]); + // Don't render if room is not in a Space if (!breadcrumb) { return null; @@ -79,24 +125,100 @@ export const SpaceBreadcrumb: React.FC = ({ roomId, classN transition={{ duration: 0.2 }} className={`flex items-center gap-2 px-4 py-2 bg-background-muted border-b border-border-default ${className}`} > - {/* Space Icon */} -
-
- -
- {breadcrumb.spaceName} + {/* Space Selector */} +
+ + + + {showSpaceDropdown && ( + +
+ {allSpaces.map((space) => ( + + ))} +
+
+ )} +
{/* Separator */} - {/* Room Name */} -
- - {breadcrumb.roomName} + {/* Room Selector */} +
+ + + + {showRoomDropdown && ( + +
+ {spaceRooms.map((room) => ( + + ))} +
+
+ )} +
- {/* Optional: Add a badge to indicate it's a Space room */} + {/* Space Room Badge */}
Space Room diff --git a/ui/desktop/src/components/TabbedChatContainer.tsx b/ui/desktop/src/components/TabbedChatContainer.tsx index a59d20b726..218f296d96 100644 --- a/ui/desktop/src/components/TabbedChatContainer.tsx +++ b/ui/desktop/src/components/TabbedChatContainer.tsx @@ -7,6 +7,7 @@ import { useTabContext } from '../contexts/TabContext'; import { ResizableSplitter } from './Layout/ResizableSplitter'; import { TaskExecutionProvider } from '../contexts/TaskExecutionContext'; import { SpaceBreadcrumb } from './SpaceBreadcrumb'; +import { useNavigate } from 'react-router-dom'; interface TabbedChatContainerProps { setIsGoosehintsModalOpen?: (isOpen: boolean) => void; @@ -23,6 +24,7 @@ export const TabbedChatContainer: React.FC = ({ initialMessage, sidebarCollapsed = false }) => { + const navigate = useNavigate(); const { tabStates, activeTabId, @@ -35,11 +37,26 @@ export const TabbedChatContainer: React.FC = ({ syncTabTitleWithBackend, updateTabTitleFromMessage, updateSessionId, + openMatrixChat, // Sidecar functions hideSidecarView, getSidecarState } = useTabContext(); + // Handle room switching from breadcrumb + const handleRoomSwitch = useCallback((roomId: string) => { + console.log('🔄 Switching to room:', roomId); + openMatrixChat(roomId); + }, [openMatrixChat]); + + // Handle space navigation from breadcrumb + const handleSpaceNavigation = useCallback((spaceId: string) => { + console.log('🏠 Navigating to space:', spaceId); + // TODO: Navigate to space view (e.g., SpaceRoomsView or ChannelsView) + // For now, we could navigate to the first room in the space + // navigate(`/channels/${spaceId}`); + }, [navigate]); + const handleMessageSubmitWrapper = useCallback(async (message: string, tabId: string) => { // Find the tab state to check if this is the first message const tabState = tabStates.find(ts => ts.tab.id === tabId); @@ -204,7 +221,11 @@ export const TabbedChatContainer: React.FC = ({ {/* Space Breadcrumb - Show for Matrix tabs with room IDs */} {activeTabState && activeTabState.tab.type === 'matrix' && activeTabState.tab.matrixRoomId && (
- +
)}