-
- {PLATFORMS.map(p => {
- const active = p.platform === platform;
- return (
-
- );
- })}
-
-
@@ -534,7 +457,9 @@ function RecentCallsSection({
)}
{rows === null ? (
-
{t('skills.meetingBots.recentCallsLoading')}
+
+ {t('skills.meetingBots.recentCallsLoading')}
+
) : rows.length === 0 ? (
{t('skills.meetingBots.recentCallsEmpty')}
@@ -567,13 +492,17 @@ function RecentCallRow({ call }: { call: MeetCallRecord }) {
return (
- {meetingCode}
+
+ {meetingCode}
+
{formatRelativeTime(call.started_at_ms)}
- {call.turn_count} turn{call.turn_count === 1 ? '' : 's'}
+
+ {call.turn_count} turn{call.turn_count === 1 ? '' : 's'}
+
{duration}s on call
diff --git a/app/src/components/skills/__tests__/MeetingBotsCard.test.tsx b/app/src/components/skills/__tests__/MeetingBotsCard.test.tsx
index b3a1f09e0..9a34aa27d 100644
--- a/app/src/components/skills/__tests__/MeetingBotsCard.test.tsx
+++ b/app/src/components/skills/__tests__/MeetingBotsCard.test.tsx
@@ -92,6 +92,48 @@ describe('MeetingBotsCard', () => {
});
});
+ it('uses the saved persona and mascot profile when joining', async () => {
+ joinMock.mockResolvedValueOnce({
+ meetUrl: 'https://meet.google.com/abc-defg-hij',
+ platform: 'gmeet',
+ });
+
+ renderWithProviders(
, {
+ preloadedState: {
+ persona: { displayName: 'Nova', description: 'Calm and concise.' },
+ mascot: {
+ color: 'custom',
+ voiceId: null,
+ voiceGender: 'male',
+ voiceUseLocaleDefault: false,
+ selectedMascotId: 'yellow',
+ customMascotGifUrl: null,
+ customPrimaryColor: '#123456',
+ customSecondaryColor: '#abcdef',
+ },
+ },
+ });
+
+ fireEvent.click(screen.getByTestId('meeting-bots-banner'));
+ fireEvent.change(screen.getByLabelText(/meeting link/i), {
+ target: { value: 'https://meet.google.com/abc-defg-hij' },
+ });
+ fireEvent.submit(screen.getByRole('dialog').querySelector('form')!);
+
+ await vi.waitFor(() => {
+ expect(joinMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ meetUrl: 'https://meet.google.com/abc-defg-hij',
+ displayName: 'Nova',
+ agentName: 'Nova',
+ systemPrompt: 'Calm and concise.',
+ mascotId: 'yellow',
+ riveColors: { primaryColor: '#123456', secondaryColor: '#abcdef' },
+ })
+ );
+ });
+ });
+
it('surfaces a join error inline + as an error toast', async () => {
joinMock.mockRejectedValueOnce(new Error('Bad URL'));
const onToast = vi.fn();
@@ -111,19 +153,21 @@ describe('MeetingBotsCard', () => {
expect(screen.getByRole('alert')).toHaveTextContent('Bad URL');
});
- it('Zoom is a live platform — submit is labelled "Send to Zoom", not "coming soon"', () => {
+ it('does not show meeting platform choices in the Google Meet CTA', () => {
renderWithProviders(
);
fireEvent.click(screen.getByTestId('meeting-bots-banner'));
- // Zoom is fully supported via Recall.ai; submit should not say "coming soon".
- fireEvent.click(screen.getByRole('button', { name: /Zoom/ }));
- expect(screen.queryByRole('button', { name: /coming soon/i })).not.toBeInTheDocument();
- expect(screen.getByRole('button', { name: /send to zoom/i })).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: /Zoom/ })).not.toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: /Microsoft Teams/ })).not.toBeInTheDocument();
+ expect(screen.getByRole('button', { name: /send to google meet/i })).toBeInTheDocument();
});
- it('does not require the old owner-name field for backend Recall joins', () => {
+ it('only asks for the meeting link, not old bot tuning fields', () => {
renderWithProviders(
);
fireEvent.click(screen.getByTestId('meeting-bots-banner'));
+ expect(screen.getByLabelText(/meeting link/i)).toBeInTheDocument();
+ expect(screen.queryByLabelText(/^display name$/i)).not.toBeInTheDocument();
expect(screen.queryByLabelText(/your name in the call/i)).not.toBeInTheDocument();
+ expect(screen.queryByLabelText(/wake phrase/i)).not.toBeInTheDocument();
});
});
@@ -201,9 +245,7 @@ describe('MeetingBotsCard — ActiveMeetingView', () => {
// MeetingBotsCard only shows ActiveMeetingView for active/joining.
// When ended the banner is rendered so the user can start a new call.
renderWithProviders(
, {
- preloadedState: {
- backendMeet: { ...activeMeetState.backendMeet, status: 'ended' as const },
- },
+ preloadedState: { backendMeet: { ...activeMeetState.backendMeet, status: 'ended' as const } },
});
expect(screen.getByTestId('meeting-bots-banner')).toBeInTheDocument();
expect(screen.queryByText(/live in meeting/i)).not.toBeInTheDocument();
@@ -212,14 +254,10 @@ describe('MeetingBotsCard — ActiveMeetingView', () => {
it('shows error toast when leave call fails', async () => {
leaveMock.mockRejectedValueOnce(new Error('Network error'));
const onToast = vi.fn();
- renderWithProviders(
, {
- preloadedState: activeMeetState,
- });
+ renderWithProviders(
, { preloadedState: activeMeetState });
fireEvent.click(screen.getByRole('button', { name: /leave/i }));
await waitFor(() =>
- expect(onToast).toHaveBeenCalledWith(
- expect.objectContaining({ type: 'error' })
- )
+ expect(onToast).toHaveBeenCalledWith(expect.objectContaining({ type: 'error' }))
);
});
});
@@ -267,8 +305,16 @@ describe('MeetingBotsModal — recent calls section', () => {
it('renders a row for each returned call record', async () => {
const records = [
- makeCallRecord({ request_id: 'req-1', meet_url: 'https://meet.google.com/aaa-bbbb-ccc', turn_count: 2 }),
- makeCallRecord({ request_id: 'req-2', meet_url: 'https://meet.google.com/ddd-eeee-fff', turn_count: 5 }),
+ makeCallRecord({
+ request_id: 'req-1',
+ meet_url: 'https://meet.google.com/aaa-bbbb-ccc',
+ turn_count: 2,
+ }),
+ makeCallRecord({
+ request_id: 'req-2',
+ meet_url: 'https://meet.google.com/ddd-eeee-fff',
+ turn_count: 5,
+ }),
];
listMock.mockResolvedValueOnce(records);
@@ -321,9 +367,7 @@ describe('MeetingBotsModal — recent calls section', () => {
});
it('shows duration as combined spoken + listened seconds', async () => {
- listMock.mockResolvedValueOnce([
- makeCallRecord({ spoken_seconds: 40, listened_seconds: 20 }),
- ]);
+ listMock.mockResolvedValueOnce([makeCallRecord({ spoken_seconds: 40, listened_seconds: 20 })]);
renderWithProviders(
{}} />);
@@ -334,9 +378,7 @@ describe('MeetingBotsModal — recent calls section', () => {
it('shows a relative timestamp for recent calls', async () => {
// started 5 minutes ago
- listMock.mockResolvedValueOnce([
- makeCallRecord({ started_at_ms: Date.now() - 5 * 60 * 1000 }),
- ]);
+ listMock.mockResolvedValueOnce([makeCallRecord({ started_at_ms: Date.now() - 5 * 60 * 1000 })]);
renderWithProviders( {}} />);
diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts
index c657d3d3c..07a87ad9d 100644
--- a/app/src/lib/i18n/ar.ts
+++ b/app/src/lib/i18n/ar.ts
@@ -273,6 +273,7 @@ const messages: TranslationMap = {
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'القنوات',
'skills.tabs.explorer': 'المهارات',
+ 'skills.tabs.meetings': 'Google Meet',
'skills.tabs.mcp': 'MCP الخوادم',
'memory.title': 'الذاكرة',
'memory.search': 'البحث في الذكريات...',
diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts
index e422f5c2c..d80241e1a 100644
--- a/app/src/lib/i18n/bn.ts
+++ b/app/src/lib/i18n/bn.ts
@@ -275,6 +275,7 @@ const messages: TranslationMap = {
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'চ্যানেল',
'skills.tabs.explorer': 'স্কিল',
+ 'skills.tabs.meetings': 'Google Meet',
'skills.tabs.mcp': 'MCP সার্ভার',
'memory.title': 'মেমোরি',
'memory.search': 'মেমোরি খুঁজুন...',
diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts
index 9e011fb04..e39e62a19 100644
--- a/app/src/lib/i18n/de.ts
+++ b/app/src/lib/i18n/de.ts
@@ -284,6 +284,7 @@ const messages: TranslationMap = {
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'Kanäle',
'skills.tabs.explorer': 'Skills',
+ 'skills.tabs.meetings': 'Google Meet',
'skills.tabs.mcp': 'MCP Server',
'memory.title': 'Erinnerung',
'memory.search': 'Erinnerungen suchen...',
diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts
index b4172b69f..1b4dd44f9 100644
--- a/app/src/lib/i18n/en.ts
+++ b/app/src/lib/i18n/en.ts
@@ -300,6 +300,7 @@ const en: TranslationMap = {
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'Channels',
'skills.tabs.explorer': 'Skills',
+ 'skills.tabs.meetings': 'Google Meet',
'skills.tabs.mcp': 'MCP Servers',
// Intelligence / Memory
'memory.title': 'Memory',
diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts
index 3cc6fe72d..984a2d008 100644
--- a/app/src/lib/i18n/es.ts
+++ b/app/src/lib/i18n/es.ts
@@ -285,6 +285,7 @@ const messages: TranslationMap = {
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'Canales',
'skills.tabs.explorer': 'Skills',
+ 'skills.tabs.meetings': 'Google Meet',
'skills.tabs.mcp': 'MCP Servidores',
'memory.title': 'Memoria',
'memory.search': 'Buscar recuerdos...',
diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts
index 355740053..4539b6c73 100644
--- a/app/src/lib/i18n/fr.ts
+++ b/app/src/lib/i18n/fr.ts
@@ -283,6 +283,7 @@ const messages: TranslationMap = {
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'Canaux',
'skills.tabs.explorer': 'Skills',
+ 'skills.tabs.meetings': 'Google Meet',
'skills.tabs.mcp': 'MCP Serveurs',
'memory.title': 'Mémoire',
'memory.search': 'Rechercher dans la mémoire…',
diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts
index f96fcffee..7ad19cdf8 100644
--- a/app/src/lib/i18n/hi.ts
+++ b/app/src/lib/i18n/hi.ts
@@ -275,6 +275,7 @@ const messages: TranslationMap = {
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'चैनल',
'skills.tabs.explorer': 'स्किल',
+ 'skills.tabs.meetings': 'Google Meet',
'skills.tabs.mcp': 'MCP सर्वर',
'memory.title': 'मेमोरी',
'memory.search': 'मेमोरी सर्च करें...',
diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts
index 81bed168e..745c540bd 100644
--- a/app/src/lib/i18n/id.ts
+++ b/app/src/lib/i18n/id.ts
@@ -277,6 +277,7 @@ const messages: TranslationMap = {
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'Saluran',
'skills.tabs.explorer': 'Skill',
+ 'skills.tabs.meetings': 'Google Meet',
'skills.tabs.mcp': 'MCP Server',
'memory.title': 'Memori',
'memory.search': 'Cari memori...',
diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts
index 5e643e924..7e1326508 100644
--- a/app/src/lib/i18n/it.ts
+++ b/app/src/lib/i18n/it.ts
@@ -281,6 +281,7 @@ const messages: TranslationMap = {
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'Canali',
'skills.tabs.explorer': 'Skill',
+ 'skills.tabs.meetings': 'Google Meet',
'skills.tabs.mcp': 'MCP Server',
'memory.title': 'Memoria',
'memory.search': 'Cerca memorie...',
diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts
index 3f2f70848..7fac6c5de 100644
--- a/app/src/lib/i18n/ko.ts
+++ b/app/src/lib/i18n/ko.ts
@@ -275,6 +275,7 @@ const messages: TranslationMap = {
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': '채널',
'skills.tabs.explorer': '스킬',
+ 'skills.tabs.meetings': 'Google Meet',
'skills.tabs.mcp': 'MCP 서버',
'memory.title': '메모리',
'memory.search': '메모리 검색...',
diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts
index 7b9598d39..d16fa3067 100644
--- a/app/src/lib/i18n/pl.ts
+++ b/app/src/lib/i18n/pl.ts
@@ -279,6 +279,7 @@ const messages: TranslationMap = {
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'Kanały',
'skills.tabs.explorer': 'Skille',
+ 'skills.tabs.meetings': 'Google Meet',
'skills.tabs.mcp': 'Serwery MCP',
'memory.title': 'Pamięć',
'memory.search': 'Szukaj w pamięci...',
diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts
index 75fd73437..9fc408cd8 100644
--- a/app/src/lib/i18n/pt.ts
+++ b/app/src/lib/i18n/pt.ts
@@ -283,6 +283,7 @@ const messages: TranslationMap = {
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'Canais',
'skills.tabs.explorer': 'Skills',
+ 'skills.tabs.meetings': 'Google Meet',
'skills.tabs.mcp': 'MCP Servidores',
'memory.title': 'Memória',
'memory.search': 'Pesquisar memórias...',
diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts
index 81d958036..89378ec8b 100644
--- a/app/src/lib/i18n/ru.ts
+++ b/app/src/lib/i18n/ru.ts
@@ -277,6 +277,7 @@ const messages: TranslationMap = {
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': 'Каналы',
'skills.tabs.explorer': 'Навыки',
+ 'skills.tabs.meetings': 'Google Meet',
'skills.tabs.mcp': 'MCP Серверы',
'memory.title': 'Память',
'memory.search': 'Поиск воспоминаний...',
diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts
index 84f2b6b23..d5866ae2c 100644
--- a/app/src/lib/i18n/zh-CN.ts
+++ b/app/src/lib/i18n/zh-CN.ts
@@ -264,6 +264,7 @@ const messages: TranslationMap = {
'skills.tabs.composio': 'Composio',
'skills.tabs.channels': '渠道',
'skills.tabs.explorer': '技能',
+ 'skills.tabs.meetings': 'Google Meet',
'skills.tabs.mcp': 'MCP 服务器',
'memory.title': '记忆',
'memory.search': '搜索记忆...',
diff --git a/app/src/pages/Skills.tsx b/app/src/pages/Skills.tsx
index e8757bd08..cb4066ea2 100644
--- a/app/src/pages/Skills.tsx
+++ b/app/src/pages/Skills.tsx
@@ -342,7 +342,7 @@ interface SkillItem {
// ─── Main Skills Page ──────────────────────────────────────────────────────────
-type ConnectionsTab = 'channels' | 'composio' | 'mcp';
+type ConnectionsTab = 'channels' | 'composio' | 'mcp' | 'meetings';
export default function Skills() {
const { t } = useT();
@@ -350,13 +350,13 @@ export default function Skills() {
const location = useLocation();
const navigate = useNavigate();
const isLocalSession = isLocalSessionToken(getCoreStateSnapshot().snapshot.sessionToken);
- // Honour `?tab=` so deep links land on the right
+ // Honour `?tab=` so deep links land on the right
// sub-tab. (The legacy `runners` tab was removed; running a workflow now
// lives on its detail drawer → /skills/run.)
const initialTab: ConnectionsTab = (() => {
const params = new URLSearchParams(location.search);
const t = params.get('tab');
- if (t === 'composio' || t === 'channels' || t === 'mcp') return t;
+ if (t === 'composio' || t === 'channels' || t === 'mcp' || t === 'meetings') return t;
return 'composio';
})();
const [activeTab, setActiveTab] = useState(initialTab);
@@ -792,6 +792,7 @@ export default function Skills() {
items={[
{ value: 'composio', label: t('skills.tabs.composio') },
{ value: 'channels', label: t('skills.tabs.channels') },
+ { value: 'meetings', label: t('skills.tabs.meetings') },
{ value: 'mcp', label: t('skills.tabs.mcp') },
]}
/>
@@ -859,7 +860,7 @@ export default function Skills() {