From dfd11a3bbb2591456f0bc90b94c25b19c7e34be9 Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Fri, 14 Nov 2025 10:45:45 -0500 Subject: [PATCH] feat: enhance Goose-to-Goose communication with bidirectional messaging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🦆 MAJOR ENHANCEMENT: Complete bidirectional Goose communication system ## Key Improvements: ### Message Handling: ✅ Process ALL messages (including from self) with isFromSelf metadata ✅ Distinguish between messages from self vs others in UI ✅ Proper sender identification and display names ✅ Echo prevention for regular chat while preserving Goose messages ### Enhanced Matrix Service: ✅ findDirectMessageRoom() - Find existing DM rooms with users ✅ getOrCreateDirectMessageRoom() - Get or create DM rooms seamlessly ✅ Enhanced message metadata with sender identification ✅ Improved message routing and event emission ### UI Enhancements: ✅ Visual distinction between sent/received messages (blue vs green) ✅ Sender name display with 'You' for self-messages ✅ One-click 'Chat' buttons next to each Goose instance ✅ Automatic DM room creation/selection ✅ Interactive collaboration invite accept/decline buttons ✅ Task response buttons for incoming task requests ### User Experience: ✅ Click 'Chat' next to any Goose → automatically sets up DM room ✅ Send messages, invites, tasks directly to specific Goose instances ✅ See your own messages and invites in the chat history ✅ Clear visual feedback for message types and senders ✅ Capability and metadata display in message details ## What This Enables: 🔄 **Full Bidirectional Communication**: See both sent and received messages 👥 **Easy Direct Messaging**: One-click chat setup with any Goose instance 📋 **Task Collaboration**: Send/receive task requests with response buttons 🤝 **Project Invites**: Send collaboration invites and see responses 🎯 **User-Friendly Interface**: Clear visual distinction and easy interaction Now you can invite someone to chat via Goose and both parties will see the invite and subsequent messages in their respective Goose interfaces! --- ui/desktop/src/components/GooseChat.tsx | 123 ++++++++++++++++++---- ui/desktop/src/contexts/MatrixContext.tsx | 12 +++ ui/desktop/src/services/MatrixService.ts | 58 +++++++--- 3 files changed, 155 insertions(+), 38 deletions(-) diff --git a/ui/desktop/src/components/GooseChat.tsx b/ui/desktop/src/components/GooseChat.tsx index 331f75219d..a51c3992be 100644 --- a/ui/desktop/src/components/GooseChat.tsx +++ b/ui/desktop/src/components/GooseChat.tsx @@ -12,6 +12,7 @@ const GooseChat: React.FC = () => { acceptCollaborationInvite, createGooseCollaborationRoom, announceCapabilities, + getOrCreateDirectMessageRoom, onGooseMessage } = useMatrix(); @@ -102,6 +103,22 @@ const GooseChat: React.FC = () => { } }; + const getSenderDisplayName = (message: GooseChatMessage) => { + const isFromSelf = message.metadata?.isFromSelf; + if (isFromSelf) { + return 'You'; + } + + // Try to find display name from goose instances + const gooseInstance = gooseInstances.find(g => g.userId === message.sender); + if (gooseInstance?.displayName) { + return gooseInstance.displayName; + } + + // Fallback to user ID without domain + return message.sender.split(':')[0].substring(1); + }; + if (!isConnected) { return (
@@ -130,6 +147,20 @@ const GooseChat: React.FC = () => { }`} /> {goose.displayName || goose.userId} {goose.status || 'idle'} +
))} @@ -227,30 +258,76 @@ const GooseChat: React.FC = () => { {messages.length === 0 ? (

No Goose messages yet. Send a message to see it here!

) : ( - messages.map((message, index) => ( -
-
- {formatMessageType(message.type)} - - {message.timestamp.toLocaleTimeString()} - + messages.map((message, index) => { + const isFromSelf = message.metadata?.isFromSelf; + const senderName = getSenderDisplayName(message); + + return ( +
+
+
+ {formatMessageType(message.type)} + + {senderName} + +
+ + {message.timestamp.toLocaleTimeString()} + +
+

{message.content}

+
+ Room: {message.roomId} + {message.metadata?.taskId && ` | Task: ${message.metadata.taskId}`} + {message.metadata?.priority && ` | Priority: ${message.metadata.priority}`} + {message.metadata?.capabilities && ` | Capabilities: ${message.metadata.capabilities.join(', ')}`} +
+ {message.type === 'goose.collaboration.invite' && !isFromSelf && ( +
+ + +
+ )} + {message.type === 'goose.task.request' && !isFromSelf && ( +
+ +
+ )}
-

{message.content}

-
- From: {message.sender} | Room: {message.roomId} - {message.metadata?.taskId && ` | Task: ${message.metadata.taskId}`} - {message.metadata?.priority && ` | Priority: ${message.metadata.priority}`} -
- {message.type === 'goose.collaboration.invite' && ( - - )} -
- )) + ); + }) )}
diff --git a/ui/desktop/src/contexts/MatrixContext.tsx b/ui/desktop/src/contexts/MatrixContext.tsx index 200b38674f..69da76e721 100644 --- a/ui/desktop/src/contexts/MatrixContext.tsx +++ b/ui/desktop/src/contexts/MatrixContext.tsx @@ -34,6 +34,8 @@ interface MatrixContextType { declineCollaborationInvite: (roomId: string, originalMessageId: string, reason?: string, metadata?: Record) => Promise; createGooseCollaborationRoom: (name: string, inviteGooseIds?: string[], topic?: string) => Promise; announceCapabilities: (roomId: string, capabilities: string[], status?: 'idle' | 'busy' | 'working', currentTask?: string) => Promise; + findDirectMessageRoom: (userId: string) => string | null; + getOrCreateDirectMessageRoom: (userId: string) => Promise; // Events onMessage: (callback: (data: any) => void) => () => void; @@ -242,6 +244,14 @@ export const MatrixProvider: React.FC = ({ children, matrix return await matrixService.announceCapabilities(roomId, capabilities, status, currentTask); }; + const findDirectMessageRoom = (userId: string) => { + return matrixService.findDirectMessageRoom(userId); + }; + + const getOrCreateDirectMessageRoom = async (userId: string) => { + return await matrixService.getOrCreateDirectMessageRoom(userId); + }; + const onGooseMessage = (callback: (message: GooseChatMessage) => void) => { matrixService.on('gooseMessage', callback); return () => matrixService.off('gooseMessage', callback); @@ -274,6 +284,8 @@ export const MatrixProvider: React.FC = ({ children, matrix declineCollaborationInvite, createGooseCollaborationRoom, announceCapabilities, + findDirectMessageRoom, + getOrCreateDirectMessageRoom, // Events onMessage, onAIMessage, diff --git a/ui/desktop/src/services/MatrixService.ts b/ui/desktop/src/services/MatrixService.ts index 7f092db9b7..e0e9080217 100644 --- a/ui/desktop/src/services/MatrixService.ts +++ b/ui/desktop/src/services/MatrixService.ts @@ -473,11 +473,7 @@ export class MatrixService extends EventEmitter { private handleMessage(event: any, room: any): void { const content = event.getContent(); const sender = event.getSender(); - - // Skip messages from ourselves to avoid echo - if (sender === this.config.userId) { - return; - } + const isFromSelf = sender === this.config.userId; const messageData = { roomId: room.roomId, @@ -485,6 +481,7 @@ export class MatrixService extends EventEmitter { content: content.body, timestamp: new Date(event.getTs()), event, + isFromSelf, }; // Check if this is a structured Goose message (new format) @@ -504,13 +501,15 @@ export class MatrixService extends EventEmitter { capabilities: content['goose.capabilities'], status: content['goose.status'], attachments: content['goose.attachments'], + isFromSelf, // Add this to metadata so UI can distinguish ...content['goose.metadata'], }, }; - console.log('🦆 Received Goose message:', gooseChatMessage.type, 'from:', sender); + console.log('🦆 Received Goose message:', gooseChatMessage.type, 'from:', sender, isFromSelf ? '(self)' : '(other)'); this.emit('gooseMessage', gooseChatMessage); - return; + + // Don't return early - let it also be processed as regular message } // Check if this is a legacy Goose AI message (using custom properties) @@ -522,16 +521,17 @@ export class MatrixService extends EventEmitter { model: content['goose.model'], sender, timestamp: new Date(content['goose.timestamp'] || event.getTs()), - metadata: content, + metadata: { ...content, isFromSelf }, }; this.emit('aiMessage', aiMessage); - return; + + // Don't return early for legacy messages either } - // Check if sender appears to be a Goose instance (heuristic detection) + // Check if sender appears to be a Goose instance (heuristic detection) - but not ourselves const senderUser = this.client?.getUser(sender); - if (this.isGooseInstance(sender, senderUser?.displayName)) { + if (!isFromSelf && this.isGooseInstance(sender, senderUser?.displayName)) { // Treat as potential Goose message even without explicit markers const gooseChatMessage: GooseChatMessage = { type: 'goose.chat', @@ -540,18 +540,20 @@ export class MatrixService extends EventEmitter { sender, timestamp: new Date(event.getTs()), roomId: room.roomId, - metadata: {}, + metadata: { isFromSelf: false }, }; console.log('🦆 Detected potential Goose message from:', sender); this.emit('gooseMessage', gooseChatMessage); } - // Always emit regular message event - this.emit('message', messageData); + // Always emit regular message event (but skip our own messages to avoid echo in regular chat) + if (!isFromSelf) { + this.emit('message', messageData); + } // Also check if this is a session-related message and emit as session message - if (content.body) { + if (content.body && !isFromSelf) { // Check for Goose session messages (from useSessionSharing) if (content.body.includes('goose-session-message:') || content.body.includes('goose-session-invite:') || @@ -759,6 +761,32 @@ export class MatrixService extends EventEmitter { return this.addFriend(userId); } + /** + * Find the room ID for a direct message with a specific user + */ + findDirectMessageRoom(userId: string): string | null { + const rooms = this.getRooms(); + const dmRoom = rooms.find(room => + room.isDirectMessage && + room.members.some(member => member.userId === userId) + ); + return dmRoom?.roomId || null; + } + + /** + * Get or create a direct message room with a user + */ + async getOrCreateDirectMessageRoom(userId: string): Promise { + // First try to find existing DM room + const existingRoomId = this.findDirectMessageRoom(userId); + if (existingRoomId) { + return existingRoomId; + } + + // Create new DM room if none exists + return this.createDirectMessage(userId); + } + /** * Logout and clear stored credentials */