Skip to content

Commit 493aeee

Browse files
authored
Implement Matrix channel creation functionality (#5962)
1 parent 4e30222 commit 493aeee

20 files changed

+3306
-602
lines changed

ui/desktop/src/App.tsx

Lines changed: 1195 additions & 322 deletions
Large diffs are not rendered by default.

ui/desktop/src/components/BaseChat.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,14 @@ function BaseChatContent({
387387
const customEvent = e as unknown as CustomEvent;
388388
const combinedTextFromInput = customEvent.detail?.value || '';
389389

390+
// DIAGNOSTIC: Log chat info when submitting message
391+
console.log('[Matrix Message Send - BaseChat] handleSubmit called:', {
392+
chatId: chat.id,
393+
chatTitle: chat.title,
394+
messagePreview: combinedTextFromInput.substring(0, 50) + '...',
395+
timestamp: new Date().toISOString(),
396+
});
397+
390398
// Mark that user has started using the recipe when they submit a message
391399
if (recipeConfig && combinedTextFromInput.trim()) {
392400
setHasStartedUsingRecipe(true);

ui/desktop/src/components/BaseChat2.tsx

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ function BaseChatContent({
114114
initialMessage,
115115
onSessionIdChange,
116116
isMatrixTab: !!matrixRoomId, // Pass Matrix tab flag based on whether we have a matrixRoomId
117+
matrixRoomId, // Pass Matrix room ID for message routing
117118
tabId, // Pass tabId for sidecar filtering
118119
});
119120

@@ -144,20 +145,26 @@ function BaseChatContent({
144145
sender: message.sender?.displayName || message.sender?.userId || 'unknown'
145146
});
146147

147-
// FIXED: Make Matrix message events SESSION-SPECIFIC to prevent cross-tab contamination
148-
// Include sessionId in the event detail so only the correct useChatStream instance processes it
148+
// FIXED: For Matrix tabs, use matrixRoomId as the routing key instead of sessionId
149+
// This ensures messages are routed correctly even if backend session ID changes
150+
const routingKey = matrixRoomId || sessionId;
151+
149152
const messageEvent = new CustomEvent('matrix-message-received', {
150153
detail: {
151154
message,
152-
targetSessionId: sessionId, // CRITICAL: Only this session should process this message
155+
targetSessionId: sessionId, // Backend session ID (for logging)
156+
targetRoomId: matrixRoomId, // Matrix room ID (for routing)
157+
routingKey: routingKey, // The actual key to match on
153158
timestamp: new Date().toISOString()
154159
}
155160
});
156161
window.dispatchEvent(messageEvent);
157162

158-
console.log('📥 BaseChat2 dispatched SESSION-SPECIFIC matrix-message-received event:', {
163+
console.log('📥 BaseChat2 dispatched matrix-message-received event:', {
159164
messageId: message.id,
160-
targetSessionId: sessionId.substring(0, 8),
165+
targetSessionId: sessionId?.substring(0, 8),
166+
targetRoomId: matrixRoomId?.substring(0, 20),
167+
routingKey: routingKey?.substring(0, 20),
161168
sender: message.sender?.displayName || message.sender?.userId || 'unknown'
162169
});
163170
}
@@ -286,17 +293,20 @@ function BaseChatContent({
286293
const shouldShowPopularTopics = showPopularTopics &&
287294
messages.length === 0 && !initialMessage && chatState === ChatState.Idle;
288295

289-
// Debug logging for empty state
290-
console.log('BaseChat2 render state:', {
291-
sessionId: sessionId, // Show full session ID for debugging
292-
sessionIdShort: sessionId.slice(0, 8), // Also show truncated for readability
296+
// DIAGNOSTIC: Enhanced logging for Matrix debugging
297+
console.log('🔍 BaseChat2 render state:', {
298+
sessionId: sessionId?.slice(0, 8) + '...', // Show truncated for readability
299+
matrixRoomId: matrixRoomId?.substring(0, 20) + '...',
300+
isMatrixTab: !!matrixRoomId,
293301
messagesLength: messages.length,
294302
chatState,
295303
shouldShowPopularTopics,
296304
loadingChat,
297305
hasSession: !!session,
298306
sessionName: session?.name,
299-
sessionDescription: session?.description
307+
sessionDescription: session?.description,
308+
showParticipantsBar,
309+
tabId
300310
});
301311

302312
// Memoize the chat object to prevent infinite re-renders

ui/desktop/src/components/ChatInput.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ interface ChatInputProps {
106106
append?: (message: Message) => void;
107107
isExtensionsLoading?: boolean;
108108
gooseEnabled?: boolean;
109+
matrixRoomId?: string | null; // Matrix room ID for this specific chat tab
109110
}
110111

111112
export default function ChatInput({
@@ -134,7 +135,13 @@ export default function ChatInput({
134135
append,
135136
isExtensionsLoading = false,
136137
gooseEnabled = true,
138+
matrixRoomId,
137139
}: ChatInputProps) {
140+
// DIAGNOSTIC: Log the matrixRoomId prop when ChatInput renders
141+
console.log('[ChatInput] Received matrixRoomId prop:', {
142+
matrixRoomId,
143+
timestamp: new Date().toISOString(),
144+
});
138145
// Track the available width for responsive layout
139146
const [availableWidth, setAvailableWidth] = useState(window.innerWidth);
140147
const chatInputRef = useRef<HTMLDivElement>(null);
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import React, { useState, useEffect } from 'react';
2+
import { ChevronRight, Hash } from 'lucide-react';
3+
import { matrixService } from '../services/MatrixService';
4+
import { motion } from 'framer-motion';
5+
6+
interface SpaceBreadcrumbProps {
7+
roomId: string;
8+
className?: string;
9+
}
10+
11+
interface BreadcrumbData {
12+
spaceName: string;
13+
spaceId: string;
14+
roomName: string;
15+
roomId: string;
16+
}
17+
18+
export const SpaceBreadcrumb: React.FC<SpaceBreadcrumbProps> = ({ roomId, className = '' }) => {
19+
const [breadcrumb, setBreadcrumb] = useState<BreadcrumbData | null>(null);
20+
21+
useEffect(() => {
22+
const loadBreadcrumb = () => {
23+
try {
24+
// Get all Spaces
25+
const spaces = matrixService.getSpaces();
26+
27+
// Find which Space contains this room
28+
for (const space of spaces) {
29+
const children = matrixService.getSpaceChildren(space.roomId);
30+
const childRoom = children.find(child => child.roomId === roomId);
31+
32+
if (childRoom) {
33+
// Found the parent Space!
34+
setBreadcrumb({
35+
spaceName: space.name || 'Unnamed Space',
36+
spaceId: space.roomId,
37+
roomName: childRoom.name || 'Unnamed Room',
38+
roomId: roomId,
39+
});
40+
return;
41+
}
42+
}
43+
44+
// Room is not in any Space
45+
setBreadcrumb(null);
46+
} catch (error) {
47+
console.error('Failed to load Space breadcrumb:', error);
48+
setBreadcrumb(null);
49+
}
50+
};
51+
52+
loadBreadcrumb();
53+
54+
// Listen for Space changes
55+
const handleSpaceUpdate = () => {
56+
loadBreadcrumb();
57+
};
58+
59+
matrixService.on('spaceChildAdded', handleSpaceUpdate);
60+
matrixService.on('spaceChildRemoved', handleSpaceUpdate);
61+
matrixService.on('ready', handleSpaceUpdate);
62+
63+
return () => {
64+
matrixService.off('spaceChildAdded', handleSpaceUpdate);
65+
matrixService.off('spaceChildRemoved', handleSpaceUpdate);
66+
matrixService.off('ready', handleSpaceUpdate);
67+
};
68+
}, [roomId]);
69+
70+
// Don't render if room is not in a Space
71+
if (!breadcrumb) {
72+
return null;
73+
}
74+
75+
return (
76+
<motion.div
77+
initial={{ opacity: 0, y: -10 }}
78+
animate={{ opacity: 1, y: 0 }}
79+
transition={{ duration: 0.2 }}
80+
className={`flex items-center gap-2 px-4 py-2 bg-background-muted border-b border-border-default ${className}`}
81+
>
82+
{/* Space Icon */}
83+
<div className="flex items-center gap-2 text-text-muted">
84+
<div className="w-5 h-5 bg-background-accent/20 rounded flex items-center justify-center">
85+
<Hash className="w-3 h-3 text-text-on-accent" />
86+
</div>
87+
<span className="text-sm font-medium">{breadcrumb.spaceName}</span>
88+
</div>
89+
90+
{/* Separator */}
91+
<ChevronRight className="w-4 h-4 text-text-muted" />
92+
93+
{/* Room Name */}
94+
<div className="flex items-center gap-2">
95+
<Hash className="w-4 h-4 text-text-muted" />
96+
<span className="text-sm text-text-default">{breadcrumb.roomName}</span>
97+
</div>
98+
99+
{/* Optional: Add a badge to indicate it's a Space room */}
100+
<div className="ml-auto">
101+
<span className="text-xs px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-600 dark:text-blue-400">
102+
Space Room
103+
</span>
104+
</div>
105+
</motion.div>
106+
);
107+
};

ui/desktop/src/components/TabbedChatContainer.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import MultiPanelTabSidecar from './MultiPanelTabSidecar';
66
import { useTabContext } from '../contexts/TabContext';
77
import { ResizableSplitter } from './Layout/ResizableSplitter';
88
import { TaskExecutionProvider } from '../contexts/TaskExecutionContext';
9+
import { SpaceBreadcrumb } from './SpaceBreadcrumb';
910

1011
interface TabbedChatContainerProps {
1112
setIsGoosehintsModalOpen?: (isOpen: boolean) => void;
@@ -200,6 +201,13 @@ export const TabbedChatContainer: React.FC<TabbedChatContainerProps> = ({
200201
/>
201202
</div>
202203

204+
{/* Space Breadcrumb - Show for Matrix tabs with room IDs */}
205+
{activeTabState && activeTabState.tab.type === 'matrix' && activeTabState.tab.matrixRoomId && (
206+
<div className="flex-shrink-0">
207+
<SpaceBreadcrumb roomId={activeTabState.tab.matrixRoomId} />
208+
</div>
209+
)}
210+
203211
{/* Main Content Area - Chat and Sidecar */}
204212
<div className="flex-1 min-h-0 relative overflow-hidden rounded-t-lg bg-background-default">
205213
{/* Render all tabs but only show the active one - this prevents unmounting */}

ui/desktop/src/components/channels/ChannelsView.tsx

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import MatrixAuth from '../peers/MatrixAuth';
1919
import { useNavigate } from 'react-router-dom';
2020
import { useTabContext } from '../../contexts/TabContext';
2121
import { matrixService } from '../../services/MatrixService';
22+
import { sessionMappingService } from '../../services/SessionMappingService';
23+
import SpaceRoomsView from './SpaceRoomsView';
2224

2325
interface Channel {
2426
roomId: string;
@@ -547,6 +549,7 @@ const ChannelsView: React.FC<ChannelsViewProps> = ({ onClose }) => {
547549
const [showMatrixAuth, setShowMatrixAuth] = useState(false);
548550
const [searchQuery, setSearchQuery] = useState('');
549551
const [favorites, setFavorites] = useState<Set<string>>(new Set());
552+
const [selectedSpace, setSelectedSpace] = useState<{ id: string; name: string } | null>(null);
550553

551554
// Load favorites from localStorage on mount
552555
useEffect(() => {
@@ -606,9 +609,11 @@ const ChannelsView: React.FC<ChannelsViewProps> = ({ onClose }) => {
606609
return mxcUrl;
607610
};
608611

609-
// Filter channels (non-DM rooms) from Matrix rooms and add favorite status
610-
const channels: Channel[] = rooms
611-
.filter(room => !room.isDirectMessage)
612+
// Get Matrix Spaces from context
613+
const { spaces } = useMatrix();
614+
615+
// Map Spaces to channels and add favorite status
616+
const channels: Channel[] = spaces
612617
.map(room => ({
613618
roomId: room.roomId,
614619
name: room.name || 'Unnamed Channel',
@@ -638,23 +643,33 @@ const ChannelsView: React.FC<ChannelsViewProps> = ({ onClose }) => {
638643

639644
const handleOpenChannel = async (channel: Channel) => {
640645
try {
641-
console.log('📱 Opening channel:', channel);
642-
643-
// Open a new tab/chat session with Matrix room parameters
644-
// Pass the channel name so it appears in the tab title
645-
openMatrixChat(channel.roomId, currentUser?.userId || '', channel.name);
646+
console.log('📦 Opening Space:', channel);
646647

647-
// Navigate to the pair view where the tabs are displayed
648-
navigate('/pair');
648+
// Set the selected space to show its rooms
649+
setSelectedSpace({ id: channel.roomId, name: channel.name });
649650
} catch (error) {
650-
console.error('Failed to open channel:', error);
651+
console.error('Failed to open Space:', error);
651652
}
652653
};
653654

654655
const handleCreateChannel = async (name: string, topic: string, isPublic: boolean) => {
655-
// TODO: Implement channel creation via Matrix service
656-
console.log('Creating channel:', { name, topic, isPublic });
657-
alert('Channel creation not yet implemented');
656+
try {
657+
console.log('📦 Creating Matrix Space:', { name, topic, isPublic });
658+
659+
// Use the new createSpace method from MatrixService
660+
const spaceId = await matrixService.createSpace(name, topic, isPublic);
661+
662+
console.log('✅ Space created successfully:', spaceId);
663+
664+
// Create session mapping for the new space
665+
const participants = [currentUser?.userId || ''];
666+
await sessionMappingService.createMappingWithBackendSession(spaceId, participants, name);
667+
668+
console.log('✅ Space creation complete');
669+
} catch (error) {
670+
console.error('❌ Failed to create Space:', error);
671+
throw error;
672+
}
658673
};
659674

660675
const handleEditChannel = (channel: Channel) => {
@@ -734,6 +749,17 @@ const ChannelsView: React.FC<ChannelsViewProps> = ({ onClose }) => {
734749
return <MatrixAuth onClose={() => setShowMatrixAuth(false)} />;
735750
}
736751

752+
// Show SpaceRoomsView if a space is selected
753+
if (selectedSpace) {
754+
return (
755+
<SpaceRoomsView
756+
spaceId={selectedSpace.id}
757+
spaceName={selectedSpace.name}
758+
onBack={() => setSelectedSpace(null)}
759+
/>
760+
);
761+
}
762+
737763
return (
738764
<div className="relative flex flex-col h-screen bg-background-muted">
739765
{/* Header */}

0 commit comments

Comments
 (0)