From e64db09e8efb4dab7c34b7b2ed8f91cf45ffc3da Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Fri, 21 Nov 2025 09:18:27 -0500 Subject: [PATCH] Fix Matrix tab persistence and real-time message handling - Implement tab persistence to prevent component unmounting when switching tabs - Remove key prop from BaseChat2 to maintain component state across tab switches - Add conditional rendering for all tabs (visible/hidden instead of mount/unmount) - Fix Matrix session ID consistency in TabContext.openMatrixChat - Enhance ChatInput Matrix room detection with TabContext integration - Add isTabActive prop to BaseChat2 for future optimizations - Ensure Matrix connections persist when navigating between tabs - Fix real-time message display in Matrix rooms when switching back to tabs - Resolve notification suppression issues for multichat sessions - Add extensive debugging logs for Matrix room detection and tab state Key fixes: * Matrix tabs no longer 'unmount' when switching away and back * Real-time messages appear correctly in background Matrix tabs * Notification suppression now works correctly with tab context * Session ID format consistency (matrix_!roomId) maintained * useSessionSharing connections persist across tab switches This resolves the core issue where Matrix tabs would lose their state and stop syncing messages after navigating to different tabs. --- ui/desktop/src/App.tsx | 22 ++- ui/desktop/src/components/BaseChat2.tsx | 9 +- ui/desktop/src/components/ChatInput.tsx | 69 ++++++- .../src/components/MessageNotification.tsx | 10 +- .../src/components/TabbedChatContainer.tsx | 146 +++++++------- ui/desktop/src/components/TabbedPairRoute.tsx | 18 +- ui/desktop/src/components/pair.tsx | 68 ++++++- ui/desktop/src/contexts/TabContext.tsx | 129 ++++++++++++- ui/desktop/src/hooks/useActiveSession.ts | 182 ++++++++++++++++-- 9 files changed, 552 insertions(+), 101 deletions(-) diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 0ae9dd9645..2bbfd055ed 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -52,7 +52,7 @@ import { NoProviderOrModelError, useAgent, } from './hooks/useAgent'; -import { TabProvider } from './contexts/TabContext'; +import { TabProvider, useTabContext } from './contexts/TabContext'; // Route Components const HubRouteWrapper = ({ @@ -367,15 +367,21 @@ export function AppInner() { const handleOpenChat = useCallback((roomId: string, senderId: string) => { console.log('📱 Opening chat for room:', roomId, 'sender:', senderId); - // For Matrix rooms (starting with !), navigate directly to pair view with Matrix parameters + // For Matrix rooms (starting with !), navigate to pair view and let the tabbed system handle it if (roomId.startsWith('!')) { console.log('📱 Opening Matrix shared session for room:', roomId); - const searchParams = new URLSearchParams({ - matrixMode: 'true', - matrixRoomId: roomId, - matrixRecipientId: senderId - }); - navigate(`/pair?${searchParams.toString()}`); + + // Navigate to pair view first (this will ensure we're in the tabbed system) + navigate('/pair'); + + // Dispatch a custom event to create a Matrix tab + // This will be handled by a component that has access to TabContext + setTimeout(() => { + const event = new CustomEvent('create-matrix-tab', { + detail: { roomId, senderId } + }); + window.dispatchEvent(event); + }, 100); // Small delay to ensure navigation completes } else { // For non-Matrix rooms, navigate to peers view navigate('/peers', { diff --git a/ui/desktop/src/components/BaseChat2.tsx b/ui/desktop/src/components/BaseChat2.tsx index 6d35899d17..d0add8c1ef 100644 --- a/ui/desktop/src/components/BaseChat2.tsx +++ b/ui/desktop/src/components/BaseChat2.tsx @@ -30,7 +30,7 @@ import PendingInvitesInHistory from './PendingInvitesInHistory'; import { useComments } from '../hooks/useComments'; interface BaseChatProps { - setChat: (chat: ChatType) => void; + setChat?: (chat: ChatType) => void; // Made optional for inactive tabs setIsGoosehintsModalOpen?: (isOpen: boolean) => void; onMessageSubmit?: (message: string) => void; renderHeader?: () => React.ReactNode; @@ -51,6 +51,8 @@ interface BaseChatProps { loadingChat?: boolean; // Tab-specific sidecar props tabId?: string; + // Tab persistence prop + isTabActive?: boolean; // Whether this tab is currently active/visible } function BaseChatContent({ @@ -284,8 +286,11 @@ function BaseChatContent({ }), [messages, recipe, session?.id, sessionId, session?.name, session?.description]); // Update parent only when session ID or title changes (to avoid infinite loops) + // Only call setChat if it's provided (active tabs) useEffect(() => { - setChat(chat); + if (setChat) { + setChat(chat); + } }, [setChat, chat.sessionId, chat.title]); const initialPrompt = messages.length == 0 && recipe?.prompt ? recipe.prompt : ''; diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index 30c01e9735..6c2f907806 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -41,6 +41,7 @@ import { CollaborativeButton } from './collaborative'; import EnhancedMentionPopover from './EnhancedMentionPopover'; import { useMatrix } from '../contexts/MatrixContext'; import { sessionMappingService } from '../services/SessionMappingService'; +import { useTabContext } from '../contexts/TabContext'; // Force rebuild timestamp: 2025-01-15T01:00:00Z - All .length errors fixed @@ -358,12 +359,71 @@ export default function ChatInput({ const [isAddCommandModalOpen, setIsAddCommandModalOpen] = useState(false); const [customCommands, setCustomCommands] = useState([]); + // Enhanced Matrix room detection with TabContext integration + const tabContext = useTabContext(); + // Check if this session is mapped from a Matrix room const matrixRoomId = sessionId ? sessionMappingService.getMatrixRoomId(sessionId) : null; - const isMatrixRoom = sessionId && (sessionId.startsWith('!') || matrixRoomId !== null); - // Get the actual Matrix room ID for useSessionSharing - const actualMatrixRoomId = sessionId && sessionId.startsWith('!') ? sessionId : matrixRoomId; + // NEW: Get Matrix room info from TabContext if available + let tabMatrixRoomId = null; + let tabMatrixRecipientId = null; + if (tabContext) { + try { + const activeTabState = tabContext.getActiveTabState(); + if (activeTabState?.tab.type === 'matrix') { + tabMatrixRoomId = activeTabState.tab.matrixRoomId || null; + tabMatrixRecipientId = activeTabState.tab.matrixRecipientId || null; + } + } catch (error) { + console.debug('TabContext not available for Matrix detection'); + } + } + + // Determine if this is a Matrix room using multiple sources + const isMatrixRoom = !!( + (sessionId && sessionId.startsWith('!')) || + (sessionId && sessionId.startsWith('matrix_!')) || + matrixRoomId || + tabMatrixRoomId + ); + + // Get the actual Matrix room ID for useSessionSharing - prioritize TabContext + let actualMatrixRoomId = null; + if (tabMatrixRoomId) { + // From active tab context (most reliable) + actualMatrixRoomId = tabMatrixRoomId; + } else if (sessionId && sessionId.startsWith('!')) { + // Direct Matrix room ID + actualMatrixRoomId = sessionId; + } else if (sessionId && sessionId.startsWith('matrix_!')) { + // Extract room ID from matrix_!roomId format + actualMatrixRoomId = sessionId.substring(7); // Remove 'matrix_' prefix + } else if (matrixRoomId) { + // From session mapping service + actualMatrixRoomId = matrixRoomId; + } + + console.log('🔍 ChatInput Matrix room detection:', { + sessionId, + matrixRoomId, + tabMatrixRoomId, + tabMatrixRecipientId, + isMatrixRoom, + actualMatrixRoomId, + sessionIdStartsWithExclamation: sessionId?.startsWith('!'), + sessionIdStartsWithMatrix: sessionId?.startsWith('matrix_!'), + detectionSource: actualMatrixRoomId === tabMatrixRoomId ? 'TabContext' : + actualMatrixRoomId === sessionId ? 'DirectSessionId' : + actualMatrixRoomId === sessionId?.substring(7) ? 'MatrixSessionId' : + actualMatrixRoomId === matrixRoomId ? 'SessionMapping' : 'None', + // Additional debugging for useSessionSharing + willPassToUseSessionSharing: { + sessionId: actualMatrixRoomId || sessionId, + initialRoomId: actualMatrixRoomId, + isMatrixMode: isMatrixRoom + } + }); // Get Matrix context for current user information const { currentUser } = useMatrix(); @@ -384,6 +444,7 @@ export default function ChatInput({ appendFunctionType: typeof append, sessionId: sessionId, isMatrixRoom: isMatrixRoom, + actualMatrixRoomId: actualMatrixRoomId, timestamp: new Date().toISOString() }); @@ -410,7 +471,7 @@ export default function ChatInput({ console.warn('⚠️ ChatInput: *** APPEND FUNCTION IS NOT AVAILABLE! ***'); } }, - initialRoomId: isMatrixRoom ? actualMatrixRoomId : null, // Pass actual Matrix room ID if it's a Matrix room + initialRoomId: actualMatrixRoomId, // FIXED: Always pass Matrix room ID if available, regardless of isMatrixRoom flag onParticipantJoin: (participant) => { console.log('👥 Participant joined session:', participant); }, diff --git a/ui/desktop/src/components/MessageNotification.tsx b/ui/desktop/src/components/MessageNotification.tsx index f0374f5e9a..7693cf0a60 100644 --- a/ui/desktop/src/components/MessageNotification.tsx +++ b/ui/desktop/src/components/MessageNotification.tsx @@ -51,7 +51,15 @@ const MessageNotification: React.FC = ({ if (sender === currentUser.userId) return; // Use the enhanced notification suppression logic - if (shouldSuppressNotification(roomId, sender)) { + const shouldSuppress = shouldSuppressNotification(roomId, sender); + console.log('🔍 MessageNotification suppression check:', { + roomId, + sender, + shouldSuppress, + currentPath: location.pathname + }); + + if (shouldSuppress) { console.log('🔕 Suppressing message notification for active session:', roomId); return; } diff --git a/ui/desktop/src/components/TabbedChatContainer.tsx b/ui/desktop/src/components/TabbedChatContainer.tsx index 5647c5ff85..11b1509734 100644 --- a/ui/desktop/src/components/TabbedChatContainer.tsx +++ b/ui/desktop/src/components/TabbedChatContainer.tsx @@ -115,6 +115,8 @@ export const TabbedChatContainer: React.FC = ({ return () => window.removeEventListener('keydown', handleKeyDown); }, [handleNewTab, handleTabClose, handleTabClick, activeTabId, tabStates]); + + // Handle session updates from BaseChat2 (when a new session gets a real backend ID) const handleSessionUpdate = useCallback((newSessionId: string, tabId: string) => { const tabState = tabStates.find(ts => ts.tab.id === tabId); @@ -198,72 +200,86 @@ export const TabbedChatContainer: React.FC = ({ {/* Main Content Area - Chat and Sidecar */}
- {activeTabState && ( - <> - {hasSidecar ? ( - /* Resizable Split Layout: Chat + Sidecar */ - handleMessageSubmitWrapper(message, activeTabState.tab.id)} - onSessionIdChange={(newSessionId) => updateSessionId(activeTabState.tab.id, newSessionId)} - suppressEmptyState={false} - showPopularTopics={true} - loadingChat={activeTabState.loadingChat} - initialMessage={initialMessage} - showParticipantsBar={activeTabState.tab.type === 'matrix'} - matrixRoomId={activeTabState.tab.matrixRoomId} - showPendingInvites={activeTabState.tab.type === 'matrix'} - tabId={activeTabState.tab.id} - /> - } - rightContent={ - sidecarState && sidecarState.activeViews.length > 1 ? ( - hideSidecarView(activeTabState.tab.id, viewId)} - tabId={activeTabState.tab.id} + {/* Render all tabs but only show the active one - this prevents unmounting */} + {tabStates.map((tabState) => { + const isActive = tabState.tab.id === activeTabId; + const tabSidecarState = getSidecarState(tabState.tab.id); + const tabHasSidecar = tabSidecarState && tabSidecarState.activeViews.length > 0; + + return ( +
+ {tabHasSidecar ? ( + /* Resizable Split Layout: Chat + Sidecar */ + handleMessageSubmitWrapper(message, tabState.tab.id) : undefined} + onSessionIdChange={isActive ? (newSessionId) => updateSessionId(tabState.tab.id, newSessionId) : undefined} + suppressEmptyState={false} + showPopularTopics={true} + loadingChat={tabState.loadingChat} + initialMessage={isActive ? initialMessage : undefined} // Only pass to active tab + showParticipantsBar={tabState.tab.type === 'matrix'} + matrixRoomId={tabState.tab.matrixRoomId} + showPendingInvites={tabState.tab.type === 'matrix'} + tabId={tabState.tab.id} + isTabActive={isActive} // New prop to indicate if tab is active /> - ) : ( - hideSidecarView(activeTabState.tab.id, viewId)} - tabId={activeTabState.tab.id} - /> - ) - } - initialLeftWidth={chatWidth} - minLeftWidth={30} - maxLeftWidth={80} - onResize={setChatWidth} - className="h-full" - floatingRight={true} - /> - ) : ( - /* Full Width Chat */ - handleMessageSubmitWrapper(message, activeTabState.tab.id)} - onSessionIdChange={(newSessionId) => updateSessionId(activeTabState.tab.id, newSessionId)} - suppressEmptyState={false} - showPopularTopics={true} - loadingChat={activeTabState.loadingChat} - initialMessage={initialMessage} - showParticipantsBar={activeTabState.tab.type === 'matrix'} - matrixRoomId={activeTabState.tab.matrixRoomId} - showPendingInvites={activeTabState.tab.type === 'matrix'} - tabId={activeTabState.tab.id} - /> - )} - - )} + } + rightContent={ + tabSidecarState && tabSidecarState.activeViews.length > 1 ? ( + hideSidecarView(tabState.tab.id, viewId) : undefined} + tabId={tabState.tab.id} + /> + ) : ( + hideSidecarView(tabState.tab.id, viewId) : undefined} + tabId={tabState.tab.id} + /> + ) + } + initialLeftWidth={chatWidth} + minLeftWidth={30} + maxLeftWidth={80} + onResize={isActive ? setChatWidth : undefined} // Only active tab can resize + className="h-full" + floatingRight={true} + /> + ) : ( + /* Full Width Chat */ + handleMessageSubmitWrapper(message, tabState.tab.id) : undefined} + onSessionIdChange={isActive ? (newSessionId) => updateSessionId(tabState.tab.id, newSessionId) : undefined} + suppressEmptyState={false} + showPopularTopics={true} + loadingChat={tabState.loadingChat} + initialMessage={isActive ? initialMessage : undefined} // Only pass to active tab + showParticipantsBar={tabState.tab.type === 'matrix'} + matrixRoomId={tabState.tab.matrixRoomId} + showPendingInvites={tabState.tab.type === 'matrix'} + tabId={tabState.tab.id} + isTabActive={isActive} // New prop to indicate if tab is active + /> + )} +
+ ); + })}
); diff --git a/ui/desktop/src/components/TabbedPairRoute.tsx b/ui/desktop/src/components/TabbedPairRoute.tsx index b572b33d84..2b568eb53b 100644 --- a/ui/desktop/src/components/TabbedPairRoute.tsx +++ b/ui/desktop/src/components/TabbedPairRoute.tsx @@ -18,7 +18,7 @@ export const TabbedPairRoute: React.FC = ({ const routeState = location.state as ViewOptions | undefined; const initialMessage = routeState?.initialMessage; const { isNavExpanded } = useNavigation(); - const { openExistingSession } = useTabContext(); + const { openExistingSession, openMatrixChat } = useTabContext(); // Handle resuming existing sessions from URL parameters useEffect(() => { @@ -35,6 +35,22 @@ export const TabbedPairRoute: React.FC = ({ } }, [searchParams, openExistingSession]); + // Handle Matrix tab creation from notifications + useEffect(() => { + const handleCreateMatrixTab = (event: CustomEvent) => { + const { roomId, senderId } = event.detail; + console.log('📱 TabbedPairRoute: Creating Matrix tab for room:', roomId, 'sender:', senderId); + + // Use the new openMatrixChat method - much simpler! + openMatrixChat(roomId, senderId); + }; + + window.addEventListener('create-matrix-tab', handleCreateMatrixTab as EventListener); + return () => { + window.removeEventListener('create-matrix-tab', handleCreateMatrixTab as EventListener); + }; + }, [openMatrixChat]); + const handleMessageSubmit = (message: string, tabId: string) => { console.log('Message submitted in tab:', tabId, message); // Here you could add analytics, logging, or other side effects diff --git a/ui/desktop/src/components/pair.tsx b/ui/desktop/src/components/pair.tsx index 51377113d8..5efa8c7374 100644 --- a/ui/desktop/src/components/pair.tsx +++ b/ui/desktop/src/components/pair.tsx @@ -181,7 +181,22 @@ export default function Pair({ console.log('✅ Matrix DM session initialized with empty messages - Matrix history will load separately'); } catch (error) { console.log('❌ Matrix DM loadCurrentChat error for new room:', error); - setFatalError(`Matrix DM init failure: ${error instanceof Error ? error.message : '' + error}`); + + // Don't set fatal error for Matrix mode - create a fallback chat instead + console.log('🔄 Backend session failed, creating fallback Matrix chat'); + const fallbackChat: ChatType = { + sessionId: matrixRoomId, // Use Matrix room ID as fallback + title: `Matrix Chat ${matrixRoomId.substring(1, 8)}`, + messages: [], // Start empty - Matrix history will be loaded separately + messageHistoryIndex: 0, + recipeConfig: null, + aiEnabled: false, // AI disabled by default for Matrix DMs + }; + + setChat(fallbackChat); + setHasInitializedChat(true); // Mark as initialized even with fallback + + console.log('✅ Matrix DM fallback chat created - Matrix history will still load'); } finally { setLoadingChat(false); } @@ -463,16 +478,15 @@ export default function Pair({ } }, [agentState, setView]); - // Load Matrix room history when in Matrix mode + // Load Matrix room history when in Matrix mode - ENHANCED VERSION useEffect(() => { const loadMatrixHistory = async () => { - // In Matrix mode, load history ONLY after backend session is initialized + // Only depend on Matrix connection state and backend session initialization if (!isMatrixMode || !matrixRoomId || !isConnected || !isReady || hasLoadedMatrixHistory || !hasInitializedChat) { return; } - // Wait for the backend session to be properly set up before loading Matrix history - console.log('📜 Loading Matrix room history for DM (after backend session init):', matrixRoomId); + console.log('📜 Loading Matrix room history and syncing to backend session:', matrixRoomId); setIsLoadingMatrixHistory(true); try { @@ -526,6 +540,48 @@ export default function Pair({ // Use centralized message management for Matrix history addMessagesToChat(gooseMessages, 'matrix-history'); + // CRITICAL: Sync Matrix history to backend session for AI context + const gooseSessionId = sessionMappingService.getGooseSessionId(matrixRoomId); + if (gooseSessionId && gooseMessages.length > 0) { + console.log('📜 Syncing Matrix history to backend session for AI context:', { + matrixRoomId: matrixRoomId.substring(0, 20) + '...', + gooseSessionId, + messageCount: gooseMessages.length + }); + + try { + // Import the API function to sync messages to backend + const { replyHandler } = await import('../api'); + + // Convert Goose messages to the format expected by the backend + const backendMessages = gooseMessages.map(msg => ({ + id: msg.id, + role: msg.role, + content: msg.content, + created: msg.created, + // Include sender info if available + ...(msg.sender && { sender: msg.sender }) + })); + + // Use replyHandler to sync the Matrix history to the backend session + // This will populate the backend session with the conversation context + await replyHandler({ + body: { + session_id: gooseSessionId, + messages: backendMessages, + }, + throwOnError: false, // Don't throw on error to prevent breaking the UI + }); + + console.log('✅ Matrix history synced to backend session successfully'); + } catch (syncError) { + console.error('❌ Failed to sync Matrix history to backend session:', syncError); + // Don't fail the entire history loading if sync fails + } + } else { + console.log('⚠️ No backend session mapping found or no messages to sync'); + } + console.log('📜 Loaded Matrix collaboration history:', gooseMessages.length, 'messages'); } else { console.log('📜 No previous messages found in Matrix room'); @@ -547,7 +603,7 @@ export default function Pair({ isConnected, isReady, hasLoadedMatrixHistory, - hasInitializedChat, // Wait for backend session to be initialized + hasInitializedChat, // Re-added dependency - need backend session before syncing getRoomHistoryAsGooseMessages, // Removed addMessagesToChat from dependencies to prevent re-render loop ]); diff --git a/ui/desktop/src/contexts/TabContext.tsx b/ui/desktop/src/contexts/TabContext.tsx index 7814bf4f3e..d15ceac783 100644 --- a/ui/desktop/src/contexts/TabContext.tsx +++ b/ui/desktop/src/contexts/TabContext.tsx @@ -26,6 +26,8 @@ interface TabContextType { updateTabTitleFromMessage: (tabId: string, message: string | any) => Promise; openExistingSession: (sessionId: string, title?: string) => void; updateSessionId: (tabId: string, newSessionId: string) => void; + // Matrix-specific methods + openMatrixChat: (roomId: string, senderId: string) => void; // Sidecar management functions showSidecarView: (tabId: string, view: TabSidecarView) => void; hideSidecarView: (tabId: string, viewId: string) => void; @@ -121,8 +123,15 @@ export const TabProvider: React.FC = ({ children }) => { }, [activeTabId]); const handleTabClick = useCallback((tabId: string) => { + console.log('🖱️ TabContext: handleTabClick called:', { + clickedTabId: tabId, + currentActiveTabId: activeTabId, + willChange: tabId !== activeTabId + }); + console.log('🖱️ TabContext: About to call setActiveTabId with:', tabId); setActiveTabId(tabId); - }, []); + console.log('🖱️ TabContext: setActiveTabId called'); + }, [activeTabId]); const handleTabClose = useCallback((tabId: string) => { setTabStates(prev => { @@ -581,6 +590,122 @@ export const TabProvider: React.FC = ({ children }) => { showSidecarView(tabId, editorView); }, [showSidecarView]); + // Open a Matrix chat in a new tab or switch to it if already open + const openMatrixChat = useCallback((roomId: string, senderId: string) => { + console.log('📱 TabContext: Opening Matrix chat for room:', roomId, 'sender:', senderId); + + // Check if we already have a tab for this Matrix room + const existingTab = tabStates.find(ts => + ts.tab.type === 'matrix' && ts.tab.matrixRoomId === roomId + ); + + if (existingTab) { + console.log('📱 Matrix room already open in tab, switching to it:', existingTab.tab.id); + console.log('📱 Existing tab details:', { + id: existingTab.tab.id, + sessionId: existingTab.tab.sessionId, + type: existingTab.tab.type, + matrixRoomId: existingTab.tab.matrixRoomId, + matrixRecipientId: existingTab.tab.matrixRecipientId, + title: existingTab.tab.title + }); + + // CRITICAL FIX: Ensure the existing tab has the correct sessionId format + // This is essential for ChatInput to detect it as a Matrix room + const expectedSessionId = `matrix_${roomId}`; + if (existingTab.tab.sessionId !== expectedSessionId) { + console.log('📱 FIXING sessionId for existing Matrix tab:', { + currentSessionId: existingTab.tab.sessionId, + expectedSessionId: expectedSessionId, + willUpdate: true + }); + + // Update the existing tab's sessionId to ensure consistency + setTabStates(prev => prev.map(ts => + ts.tab.id === existingTab.tab.id + ? { + ...ts, + tab: { ...ts.tab, sessionId: expectedSessionId }, + chat: { ...ts.chat, sessionId: expectedSessionId } + } + : ts + )); + } + + setActiveTabId(existingTab.tab.id); + return; + } + + // Create a new Matrix tab + const timestamp = Date.now(); + const random = Math.random().toString(36).substr(2, 9); + const senderName = senderId.split(':')[0].substring(1); + const matrixSessionId = `matrix_${roomId}`; + const tabTitle = `Chat with ${senderName}`; + + console.log('📱 About to create new Matrix tab with:', { + matrixSessionId, + tabTitle, + roomId, + senderId, + type: 'matrix' + }); + + const newTab = createNewTab({ + sessionId: matrixSessionId, + title: tabTitle, + type: 'matrix', + matrixRoomId: roomId, + matrixRecipientId: senderId, + isActive: true + }); + + console.log('📱 Created newTab object:', { + id: newTab.id, + sessionId: newTab.sessionId, + type: newTab.type, + matrixRoomId: newTab.matrixRoomId, + matrixRecipientId: newTab.matrixRecipientId, + title: newTab.title, + fullTab: newTab + }); + + const newTabState: TabState = { + tab: newTab, + chat: { + sessionId: matrixSessionId, + title: tabTitle, + messages: [], + messageHistoryIndex: 0, + recipeConfig: null, + aiEnabled: false, // Matrix chats have AI disabled by default + }, + loadingChat: false + }; + + console.log('📱 Creating new Matrix tab state:', { + tabId: newTab.id, + sessionId: matrixSessionId, + title: tabTitle, + roomId, + senderId, + fullTabState: newTabState + }); + + setTabStates(prev => { + const newStates = [...prev, newTabState]; + console.log('📱 Updated tab states:', newStates.map(ts => ({ + id: ts.tab.id, + sessionId: ts.tab.sessionId, + type: ts.tab.type, + matrixRoomId: ts.tab.matrixRoomId, + title: ts.tab.title + }))); + return newStates; + }); + setActiveTabId(newTab.id); + }, [tabStates]); + const contextValue: TabContextType = { tabStates, activeTabId, @@ -597,6 +722,8 @@ export const TabProvider: React.FC = ({ children }) => { updateTabTitleFromMessage, openExistingSession, updateSessionId, + // Matrix-specific methods + openMatrixChat, // Sidecar functions showSidecarView, hideSidecarView, diff --git a/ui/desktop/src/hooks/useActiveSession.ts b/ui/desktop/src/hooks/useActiveSession.ts index a8f2d18fc5..940eb2ae12 100644 --- a/ui/desktop/src/hooks/useActiveSession.ts +++ b/ui/desktop/src/hooks/useActiveSession.ts @@ -1,16 +1,22 @@ import { useLocation } from 'react-router-dom'; import { useChatContext } from '../contexts/ChatContext'; +import { useTabContext } from '../contexts/TabContext'; /** * Hook to detect the currently active session/room to prevent notifications * for messages from the room the user is currently viewing + * + * Updated for new tabbed architecture - checks both legacy URL params and new tab state */ export const useActiveSession = () => { const location = useLocation(); const chatContext = useChatContext(); + + // NEW: Access tab context for tabbed architecture + const tabContext = useTabContext(); - // Get Matrix room information from URL parameters - const getActiveMatrixRoom = () => { + // Get Matrix room information from URL parameters (legacy support) + const getActiveMatrixRoomFromURL = () => { const searchParams = new URLSearchParams(location.search); const isMatrixMode = searchParams.get('matrixMode') === 'true'; const matrixRoomId = searchParams.get('matrixRoomId'); @@ -18,38 +24,185 @@ export const useActiveSession = () => { return isMatrixMode && matrixRoomId ? matrixRoomId : null; }; - // Get current session ID from chat context + // NEW: Get Matrix room information from active tab state + const getActiveMatrixRoomFromTab = () => { + if (!tabContext) return null; + + try { + const activeTabState = tabContext.getActiveTabState(); + // FIXED: Only return Matrix room ID if the CURRENTLY ACTIVE tab is a Matrix tab + // getActiveTabState() already returns the currently active tab + if (activeTabState?.tab.type === 'matrix' && activeTabState.tab.matrixRoomId) { + return activeTabState.tab.matrixRoomId; + } + } catch (error) { + // Tab context might not be available in all routes + console.debug('Tab context not available for Matrix room detection'); + } + + return null; + }; + + // Combined Matrix room detection (tab state takes priority over URL) + const getActiveMatrixRoom = () => { + return getActiveMatrixRoomFromTab() || getActiveMatrixRoomFromURL(); + }; + + // Get current session ID from chat context (legacy) or tab context (new) const getActiveSessionId = () => { + // Try tab context first (new architecture) + if (tabContext) { + try { + const activeTabState = tabContext.getActiveTabState(); + if (activeTabState?.tab.sessionId) { + return activeTabState.tab.sessionId; + } + } catch (error) { + console.debug('Tab context not available for session ID detection'); + } + } + + // Fallback to chat context (legacy) return chatContext?.chat?.sessionId || null; }; - // Get current page/view information + // NEW: Get all active Matrix rooms from all tabs + const getAllActiveMatrixRooms = () => { + if (!tabContext) return []; + + try { + return tabContext.tabStates + .filter(ts => ts.tab.type === 'matrix' && ts.tab.matrixRoomId) + .map(ts => ts.tab.matrixRoomId!); + } catch (error) { + console.debug('Tab context not available for all Matrix rooms detection'); + return []; + } + }; + + // Get current page/view information (enhanced for tabbed architecture) const getCurrentView = () => { const path = location.pathname; const searchParams = new URLSearchParams(location.search); - return { + // Legacy URL-based detection + const urlMatrixMode = searchParams.get('matrixMode') === 'true'; + const urlMatrixRoomId = searchParams.get('matrixRoomId'); + const urlMatrixRecipientId = searchParams.get('matrixRecipientId'); + + // NEW: Tab-based detection + let tabMatrixRoomId = null; + let tabMatrixRecipientId = null; + let isTabMatrixMode = false; + + if (tabContext) { + try { + const activeTabState = tabContext.getActiveTabState(); + console.log('🔍 getCurrentView tab detection:', { + activeTabId: tabContext.activeTabId, + activeTabState: activeTabState ? { + id: activeTabState.tab.id, + type: activeTabState.tab.type, + title: activeTabState.tab.title, + matrixRoomId: activeTabState.tab.matrixRoomId, + matrixRecipientId: activeTabState.tab.matrixRecipientId + } : null, + // Show all tabs to understand the full state + allTabs: tabContext.tabStates.map(ts => ({ + id: ts.tab.id, + type: ts.tab.type, + title: ts.tab.title, + isActive: ts.tab.isActive, + matrixRoomId: ts.tab.matrixRoomId + })), + // Debug timing + timestamp: new Date().toISOString(), + caller: 'getCurrentView' + }); + + if (activeTabState?.tab.type === 'matrix') { + isTabMatrixMode = true; + tabMatrixRoomId = activeTabState.tab.matrixRoomId || null; + tabMatrixRecipientId = activeTabState.tab.matrixRecipientId || null; + console.log('🔍 Matrix tab detected as active:', { + tabMatrixRoomId, + tabMatrixRecipientId + }); + } else { + console.log('🔍 Active tab is not Matrix type:', activeTabState?.tab.type); + } + } catch (error) { + console.debug('Tab context not available for current view detection'); + } + } + + const result = { path, - isMatrixMode: searchParams.get('matrixMode') === 'true', - matrixRoomId: searchParams.get('matrixRoomId'), - matrixRecipientId: searchParams.get('matrixRecipientId'), + // FIXED: In tabbed mode, ONLY use tab state, ignore URL params completely + // URL params are legacy and can be stale in tabbed environment + isMatrixMode: tabContext ? isTabMatrixMode : urlMatrixMode, + matrixRoomId: tabContext ? tabMatrixRoomId : urlMatrixRoomId, + matrixRecipientId: tabContext ? tabMatrixRecipientId : urlMatrixRecipientId, sessionId: getActiveSessionId(), + // NEW: Additional tab context info + isTabbed: !!tabContext, + allActiveMatrixRooms: getAllActiveMatrixRooms(), }; + + console.log('🔍 getCurrentView result:', result); + return result; }; // Check if a message should be suppressed (no notification shown) const shouldSuppressNotification = (messageRoomId: string, messageSenderId?: string) => { const currentView = getCurrentView(); - // If we're in Matrix mode and the message is from the current Matrix room, suppress it + console.log('🔍 shouldSuppressNotification check:', { + messageRoomId, + messageSenderId, + currentView: { + path: currentView.path, + isMatrixMode: currentView.isMatrixMode, + matrixRoomId: currentView.matrixRoomId, + matrixRecipientId: currentView.matrixRecipientId, + isTabbed: currentView.isTabbed, + allActiveMatrixRooms: currentView.allActiveMatrixRooms + } + }); + + // FIXED: Only suppress if the message is from the CURRENTLY ACTIVE Matrix room tab + // Not just any open tab - only if you're actively viewing that specific room if (currentView.isMatrixMode && currentView.matrixRoomId === messageRoomId) { - console.log('🔕 Suppressing notification: message from current Matrix room', { + console.log('🔕 Suppressing notification: message from currently active Matrix room', { messageRoomId, - currentMatrixRoomId: currentView.matrixRoomId, + currentActiveMatrixRoomId: currentView.matrixRoomId, path: currentView.path }); return true; } + + // DEBUG: Check if we're detecting the Matrix room correctly in tab context + const activeTabState = tabContext?.getActiveTabState(); + console.log('🔍 Matrix room detection debug:', { + messageRoomId, + isMatrixMode: currentView.isMatrixMode, + currentMatrixRoomId: currentView.matrixRoomId, + tabMatrixRoomId: getActiveMatrixRoomFromTab(), + urlMatrixRoomId: getActiveMatrixRoomFromURL(), + shouldSuppressBasedOnActiveRoom: currentView.isMatrixMode && currentView.matrixRoomId === messageRoomId, + // Additional debug info + activeTabId: tabContext?.activeTabId, + activeTabType: activeTabState?.tab.type, + activeTabMatrixRoomId: activeTabState?.tab.matrixRoomId, + activeTabTitle: activeTabState?.tab.title, + allTabStates: tabContext?.tabStates.map(ts => ({ + id: ts.tab.id, + type: ts.tab.type, + title: ts.tab.title, + matrixRoomId: ts.tab.matrixRoomId, + isActive: ts.tab.isActive + })) + }); // If we're in a regular chat session and have a session mapping to this Matrix room, suppress it if (currentView.sessionId && messageRoomId) { @@ -57,11 +210,11 @@ export const useActiveSession = () => { // For now, we rely on the Matrix room check above } - // If we're in pair view with a specific recipient and the message is from that recipient, suppress it + // Legacy: If we're in pair view with a specific recipient and the message is from that recipient, suppress it if (currentView.path.startsWith('/pair') && currentView.matrixRecipientId && messageSenderId === currentView.matrixRecipientId) { - console.log('🔕 Suppressing notification: message from current pair recipient', { + console.log('🔕 Suppressing notification: message from current pair recipient (legacy)', { messageSenderId, currentRecipientId: currentView.matrixRecipientId, path: currentView.path @@ -70,6 +223,7 @@ export const useActiveSession = () => { } // Don't suppress - show the notification + console.log('✅ Not suppressing notification: no active session match'); return false; }; @@ -78,5 +232,7 @@ export const useActiveSession = () => { getActiveSessionId, getCurrentView, shouldSuppressNotification, + // NEW: Additional methods for tabbed architecture + getAllActiveMatrixRooms, }; };