From df59accb817ceb9709c9e6229f4006f2eeb93211 Mon Sep 17 00:00:00 2001 From: Jason Tudisco Date: Sat, 7 Mar 2026 07:27:17 -0600 Subject: [PATCH] feat: add room member list, AI agent naming, and fix invite flow - Add clickable member count in room header that shows a dropdown with all room members (AI agent + humans) with role badges - Give each room's AI agent a random name from 10 options (Nova, Atlas, Sage, etc.), editable when creating the room - Replace AI text avatar with a robot SVG icon across all components - Fix broken invite system: add client-side URL routing for /invite/:token, handle post-login invite acceptance via sessionStorage, and return room_id from accept_invite endpoint for auto-navigation Co-Authored-By: Claude Opus 4.6 --- client/src/components/app.riot | 47 +++++- client/src/components/chat-room.riot | 169 ++++++++++++++++++- client/src/components/create-room-modal.riot | 17 ++ client/src/components/message-bubble.riot | 9 +- server/migrations/004_ai_name.sql | 2 + server/src/handlers/invites.rs | 9 +- server/src/handlers/rooms.rs | 6 +- server/src/handlers/ws.rs | 10 +- server/src/main.rs | 10 ++ server/src/models/mod.rs | 16 ++ 10 files changed, 276 insertions(+), 19 deletions(-) create mode 100644 server/migrations/004_ai_name.sql diff --git a/client/src/components/app.riot b/client/src/components/app.riot index 8d94548..9fd1dee 100644 --- a/client/src/components/app.riot +++ b/client/src/components/app.riot @@ -168,6 +168,9 @@ if (user && isAuthenticated()) { this.update({ user }) this.initChat() + } else { + // Not logged in — store invite token so we can accept after login + this.checkPendingInvite() } }, @@ -216,7 +219,7 @@ id: this._streamMsgId, room_id: msg.room_id, sender_id: 'ai-assistant', - sender_name: 'AI Assistant', + sender_name: this.state.activeRoom?.ai_name || 'AI Assistant', content: this._streamContent, mentions: [], is_ai: true, @@ -301,6 +304,9 @@ } catch (e) { console.error('Failed to load rooms:', e) } + + // Process any pending invite token + await this.processInviteToken() }, handleLogin(data) { @@ -381,6 +387,45 @@ this.update({ messages: [], showClearModal: false }) }, + /** Check URL for /invite/:token and stash it for after login if needed */ + checkPendingInvite() { + const match = window.location.pathname.match(/^\/invite\/(.+)$/) + if (match) { + sessionStorage.setItem('pendingInvite', match[1]) + // Clean URL so it doesn't confuse things + window.history.replaceState(null, '', '/') + } + }, + + /** Accept an invite token — called after login + rooms load */ + async processInviteToken() { + // Check URL first, then sessionStorage (for post-login flow) + let token = null + const urlMatch = window.location.pathname.match(/^\/invite\/(.+)$/) + if (urlMatch) { + token = urlMatch[1] + window.history.replaceState(null, '', '/') + } else { + token = sessionStorage.getItem('pendingInvite') + } + + if (!token) return + sessionStorage.removeItem('pendingInvite') + + try { + const result = await api.acceptInvite(token) + // Refresh room list — the accepted room should now appear + const rooms = await api.listRooms() + this.update({ rooms }) + // Auto-select the room the user just joined + if (result?.room_id) { + this.selectRoom(result.room_id) + } + } catch (e) { + console.error('Failed to accept invite:', e) + } + }, + scrollToBottom() { requestAnimationFrame(() => { const container = document.querySelector('.messages-list') diff --git a/client/src/components/chat-room.riot b/client/src/components/chat-room.riot index 0f78277..396751f 100644 --- a/client/src/components/chat-room.riot +++ b/client/src/components/chat-room.riot @@ -4,9 +4,18 @@

{props.room?.name}

- - {props.room?.model_id?.split('/').pop()} · {props.room?.members?.length || 0} members - +
+ + +
+
+
+ + + + + + + +
+ {props.room?.ai_name || 'AI Assistant'} + AI +
+
+
{member.display_name?.charAt(0).toUpperCase()}
+ {member.display_name} + Owner +
+
@@ -51,7 +82,15 @@
-
AI
+
+ + + + + + + +