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
*/