From 59502c21e2ece168aa791f78ae219c6d71de6c46 Mon Sep 17 00:00:00 2001 From: Cyrus Gray <144336577+graycyrus@users.noreply.github.com> Date: Mon, 29 Jun 2026 20:27:20 +0530 Subject: [PATCH] feat(meet): auto-fill meeting display name from connected account (#4297) --- app/src/components/skills/MeetingBotsCard.tsx | 66 ++++++++++++++++++- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/app/src/components/skills/MeetingBotsCard.tsx b/app/src/components/skills/MeetingBotsCard.tsx index 6c059982f..90dc90249 100644 --- a/app/src/components/skills/MeetingBotsCard.tsx +++ b/app/src/components/skills/MeetingBotsCard.tsx @@ -2,6 +2,8 @@ import debug from 'debug'; import { type RefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { type MascotFace, RiveMascot } from '../../features/human/Mascot'; +import { useComposioIntegrations } from '../../lib/composio/hooks'; +import type { ComposioConnection } from '../../lib/composio/types'; import { useT } from '../../lib/i18n/I18nContext'; import { isCapacityGateMessage, @@ -9,6 +11,7 @@ import { leaveBackendMeetBot, listMeetCalls, type MeetCallRecord, + type MeetingPlatform, } from '../../services/meetCallService'; import { type BackendMeetHarnessEvent, @@ -38,6 +41,46 @@ type Toast = { type: 'success' | 'error' | 'info'; title: string; message?: stri const log = debug('meeting-bots'); +// Composio only hands back a connected account's email — there is no separate +// display-name field on `ComposioConnection`. A meeting display name is almost +// always "First Last" derived from that account, so we best-effort humanize the +// email's local part (`first.last` → `First Last`). +function deriveDisplayNameFromEmail(email: string | undefined): string { + const localPart = email?.split('@')[0]?.trim(); + if (!localPart) return ''; + return localPart + .split(/[._-]+/) + .filter(Boolean) + .map(token => token.charAt(0).toUpperCase() + token.slice(1)) + .join(' '); +} + +// Per-platform priority of Composio toolkits to source the user's in-call +// display name from: the platform's own connected account first, then the +// mailbox, then blank. Slugs are canonical Composio slugs (see +// `canonicalizeComposioToolkitSlug`). +const NAME_SOURCE_TOOLKITS: Record = { + gmeet: ['googlemeet', 'gmail'], + zoom: ['zoom', 'gmail'], + teams: ['microsoft_teams', 'outlook', 'gmail'], + webex: ['webex', 'gmail'], +}; + +// Resolve a default "Your name in this meeting" for the given platform: walk +// that platform's toolkit priority (own account → mail → blank) and return the +// first connected account whose email yields a usable name; blank if none are +// connected. The single entry point the form calls. +function resolveMeetingDisplayName( + platform: MeetingPlatform, + connectionByToolkit: Map +): string { + for (const slug of NAME_SOURCE_TOOLKITS[platform]) { + const name = deriveDisplayNameFromEmail(connectionByToolkit.get(slug)?.accountEmail); + if (name) return name; + } + return ''; +} + interface Props { onToast?: (toast: Toast) => void; } @@ -205,6 +248,9 @@ function MeetingBotsInline({ onToast, hasSubmittedRef }: MeetingBotsInlineProps) // backend join payload as `respondToParticipant` → `respondTo`, which the // meeting stream uses to gate in-call requests to this speaker only. const [respondTo, setRespondTo] = useState(''); + // Once the user types in the name field we stop auto-prefilling it, so a + // late-arriving Composio fetch (it polls) can never clobber manual input. + const respondToTouchedRef = useRef(false); // Active (respond when addressed) vs listen-only (transcribe only). Defaults // to active; the bot still only replies after being addressed by the wake // phrase. Forwarded to the backend as `listenOnly` and mirrored into the @@ -222,6 +268,19 @@ function MeetingBotsInline({ onToast, hasSubmittedRef }: MeetingBotsInlineProps) const meetError = useAppSelector(selectBackendMeetError); const [recentCalls, setRecentCalls] = useState(null); const [recentError, setRecentError] = useState(null); + // The meeting platform this form sends to (only Google Meet is wired up for + // now). Drives both the join payload and which connected accounts we source + // the default display name from. + const platform: MeetingPlatform = 'gmeet'; + // Prefill "Your name in this meeting" from the connected account that best + // matches this platform (calendar → mail → platform-native). + const { connectionByToolkit } = useComposioIntegrations(); + const resolvedDisplayName = resolveMeetingDisplayName(platform, connectionByToolkit); + + useEffect(() => { + if (respondToTouchedRef.current || !resolvedDisplayName) return; + setRespondTo(prev => (prev.trim() ? prev : resolvedDisplayName)); + }, [resolvedDisplayName]); const refreshRecentCalls = useCallback(async () => { setRecentError(null); @@ -295,7 +354,7 @@ function MeetingBotsInline({ onToast, hasSubmittedRef }: MeetingBotsInlineProps) await joinMeetViaBackendBot({ meetUrl, displayName: agentName, - platform: 'gmeet', + platform, agentName, systemPrompt, mascotId, @@ -356,7 +415,10 @@ function MeetingBotsInline({ onToast, hasSubmittedRef }: MeetingBotsInlineProps) autoComplete="off" spellCheck={false} value={respondTo} - onChange={e => setRespondTo(e.target.value)} + onChange={e => { + respondToTouchedRef.current = true; + setRespondTo(e.target.value); + }} placeholder={t('skills.meetingBots.respondToParticipantHint')} disabled={submitting} required