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.
This commit is contained in:
spencrmartin
2025-11-21 09:18:27 -05:00
parent 1fe3edb328
commit e64db09e8e
9 changed files with 552 additions and 101 deletions
+14 -8
View File
@@ -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', {
+7 -2
View File
@@ -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 : '';
+65 -4
View File
@@ -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<CustomCommand[]>([]);
// 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);
},
@@ -51,7 +51,15 @@ const MessageNotification: React.FC<MessageNotificationProps> = ({
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;
}
@@ -115,6 +115,8 @@ export const TabbedChatContainer: React.FC<TabbedChatContainerProps> = ({
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<TabbedChatContainerProps> = ({
{/* Main Content Area - Chat and Sidecar */}
<div className="flex-1 min-h-0 relative overflow-hidden rounded-t-lg bg-background-default">
{activeTabState && (
<>
{hasSidecar ? (
/* Resizable Split Layout: Chat + Sidecar */
<ResizableSplitter
leftContent={
<BaseChat2
key={activeTabState.tab.id}
sessionId={activeTabState.tab.sessionId}
setChat={handleSetChat}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
onMessageSubmit={(message) => 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 ? (
<MultiPanelTabSidecar
sidecarState={sidecarState}
onHideView={(viewId) => 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 (
<div
key={tabState.tab.id}
className={`absolute inset-0 ${isActive ? 'block' : 'hidden'}`}
style={{
visibility: isActive ? 'visible' : 'hidden',
pointerEvents: isActive ? 'auto' : 'none'
}}
>
{tabHasSidecar ? (
/* Resizable Split Layout: Chat + Sidecar */
<ResizableSplitter
leftContent={
<BaseChat2
sessionId={tabState.tab.sessionId}
setChat={isActive ? handleSetChat : undefined} // Only active tab can update chat
setIsGoosehintsModalOpen={isActive ? setIsGoosehintsModalOpen : undefined}
onMessageSubmit={isActive ? (message) => 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
/>
) : (
<TabSidecar
sidecarState={sidecarState}
onHideView={(viewId) => 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 */
<BaseChat2
key={activeTabState.tab.id}
sessionId={activeTabState.tab.sessionId}
setChat={handleSetChat}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
onMessageSubmit={(message) => 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 ? (
<MultiPanelTabSidecar
sidecarState={tabSidecarState}
onHideView={isActive ? (viewId) => hideSidecarView(tabState.tab.id, viewId) : undefined}
tabId={tabState.tab.id}
/>
) : (
<TabSidecar
sidecarState={tabSidecarState}
onHideView={isActive ? (viewId) => 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 */
<BaseChat2
sessionId={tabState.tab.sessionId}
setChat={isActive ? handleSetChat : undefined} // Only active tab can update chat
setIsGoosehintsModalOpen={isActive ? setIsGoosehintsModalOpen : undefined}
onMessageSubmit={isActive ? (message) => 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
/>
)}
</div>
);
})}
</div>
</div>
);
+17 -1
View File
@@ -18,7 +18,7 @@ export const TabbedPairRoute: React.FC<TabbedPairRouteProps> = ({
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<TabbedPairRouteProps> = ({
}
}, [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
+62 -6
View File
@@ -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
]);
+128 -1
View File
@@ -26,6 +26,8 @@ interface TabContextType {
updateTabTitleFromMessage: (tabId: string, message: string | any) => Promise<void>;
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<TabProviderProps> = ({ 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<TabProviderProps> = ({ 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<TabProviderProps> = ({ children }) => {
updateTabTitleFromMessage,
openExistingSession,
updateSessionId,
// Matrix-specific methods
openMatrixChat,
// Sidecar functions
showSidecarView,
hideSidecarView,
+169 -13
View File
@@ -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,
};
};