mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
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:
+14
-8
@@ -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', {
|
||||
|
||||
@@ -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 : '';
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
]);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user