From f8659c00caf6cc2301fdb744aa61bce909211b6d Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Thu, 21 May 2026 14:05:19 +0530 Subject: [PATCH 1/5] refactor(meeting-bots): export MeetingBotsModal for reuse outside Skills Other surfaces (e.g. the /human mascot page) want the same modal-driven backend-bot join flow without the banner chrome. Promote the inner MeetingBotsModal to a named export and cover the standalone usage with a unit test so future surfaces have a stable contract. --- app/src/components/skills/MeetingBotsCard.tsx | 2 +- .../skills/__tests__/MeetingBotsCard.test.tsx | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/src/components/skills/MeetingBotsCard.tsx b/app/src/components/skills/MeetingBotsCard.tsx index c93dd1f26a..af5a0de44f 100644 --- a/app/src/components/skills/MeetingBotsCard.tsx +++ b/app/src/components/skills/MeetingBotsCard.tsx @@ -115,7 +115,7 @@ interface ModalProps { onToast?: (toast: Toast) => void; } -function MeetingBotsModal({ onClose, onToast }: ModalProps) { +export function MeetingBotsModal({ onClose, onToast }: ModalProps) { const { t } = useT(); const [platform, setPlatform] = useState('gmeet'); const [meetUrl, setMeetUrl] = useState(''); diff --git a/app/src/components/skills/__tests__/MeetingBotsCard.test.tsx b/app/src/components/skills/__tests__/MeetingBotsCard.test.tsx index 1d99f8d739..fb5fd4c412 100644 --- a/app/src/components/skills/__tests__/MeetingBotsCard.test.tsx +++ b/app/src/components/skills/__tests__/MeetingBotsCard.test.tsx @@ -1,7 +1,7 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import MeetingBotsCard from '../MeetingBotsCard'; +import MeetingBotsCard, { MeetingBotsModal } from '../MeetingBotsCard'; const joinMock = vi.fn(); @@ -126,4 +126,14 @@ describe('MeetingBotsCard', () => { const submit = screen.getByRole('button', { name: /coming soon/i }); expect(submit).toBeDisabled(); }); + + // MeetingBotsModal is exported standalone so HumanPage (and any future + // surface) can open the same join flow without the banner chrome. + it('exposes MeetingBotsModal as a standalone export usable without the banner', () => { + const onClose = vi.fn(); + render(); + expect(screen.getByRole('dialog')).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: 'Cancel' })); + expect(onClose).toHaveBeenCalledTimes(1); + }); }); From 8ec02a22f7d5f1777465e1cf73c98de9e4cbb935 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Thu, 21 May 2026 14:05:26 +0530 Subject: [PATCH 2/5] feat(skills): re-enable MeetingBotsCard banner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #2152 commented out the banner alongside the broader "Calls tab → Coming Soon" rollback. The backend mascot bot is the right architecture for "mascot joins a meeting" because Camoufox runs as a separate participant (its own IP, its own audio identity) so there is no echo against the user's system mic — the symptom that motivated #2152's rollback of the local-CEF join flow. Bring the banner back. --- app/src/pages/Skills.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/pages/Skills.tsx b/app/src/pages/Skills.tsx index 16a8a020c5..db7f1d7176 100644 --- a/app/src/pages/Skills.tsx +++ b/app/src/pages/Skills.tsx @@ -12,7 +12,7 @@ import { ToastContainer } from '../components/intelligence/Toast'; import AutocompleteSetupModal from '../components/skills/AutocompleteSetupModal'; import CreateSkillModal from '../components/skills/CreateSkillModal'; import InstallSkillDialog from '../components/skills/InstallSkillDialog'; -// import MeetingBotsCard from '../components/skills/MeetingBotsCard'; +import MeetingBotsCard from '../components/skills/MeetingBotsCard'; import ScreenIntelligenceSetupModal from '../components/skills/ScreenIntelligenceSetupModal'; import UnifiedSkillCard from '../components/skills/SkillCard'; import { SKILL_CATEGORY_ORDER, type SkillCategory } from '../components/skills/skillCategories'; @@ -838,7 +838,7 @@ export default function Skills() { )} - {/* */} +
From 63992826e9b701643e652f313fcd6f4abdccc0be Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Thu, 21 May 2026 14:05:32 +0530 Subject: [PATCH 3/5] feat(human): join-meeting pill that opens the backend-bot modal Surface the meeting-bot join flow directly on the mascot home (/human) so it is discoverable without digging into Skills. A small primary button next to the speak-replies toggle opens the same MeetingBotsModal component the Skills banner uses, keeping the submit, capacity-gating, and i18n behaviour identical across both surfaces. --- app/src/features/human/HumanPage.test.tsx | 24 +++++++++++++++++++++++ app/src/features/human/HumanPage.tsx | 16 +++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/app/src/features/human/HumanPage.test.tsx b/app/src/features/human/HumanPage.test.tsx index 50ecd2014a..33b88e2878 100644 --- a/app/src/features/human/HumanPage.test.tsx +++ b/app/src/features/human/HumanPage.test.tsx @@ -32,6 +32,21 @@ vi.mock('./Mascot', () => ({ vi.mock('./useHumanMascot', () => ({ useHumanMascot: () => ({ face: 'idle', visemes: [] }) })); +// Stub the meeting-bots modal — the real component imports apiClient and a +// network-aware service singleton. The HumanPage tests only care that the pill +// toggles the modal open, not the modal's submit machinery (that lives in +// MeetingBotsCard.test.tsx). +vi.mock('../../components/skills/MeetingBotsCard', () => ({ + default: () => null, + MeetingBotsModal: ({ onClose }: { onClose: () => void }) => ( +
+ +
+ ), +})); + const SPEAK_REPLIES_KEY = 'human.speakReplies'; function buildMinimalStore() { @@ -102,6 +117,15 @@ describe('HumanPage — speak-replies localStorage persistence', () => { expect(checkbox).toBeChecked(); }); + it('opens the meeting-bots modal when the join-meeting pill is clicked', async () => { + renderHumanPage(); + expect(screen.queryByTestId('meeting-bots-modal-stub')).not.toBeInTheDocument(); + await act(async () => { + fireEvent.click(screen.getByTestId('human-join-meeting-pill')); + }); + expect(screen.getByTestId('meeting-bots-modal-stub')).toBeInTheDocument(); + }); + it('renders sub-mascots for the selected thread subagent timeline', () => { const store = buildMinimalStore(); store.dispatch(setSelectedThread('thread-subagents')); diff --git a/app/src/features/human/HumanPage.tsx b/app/src/features/human/HumanPage.tsx index 8def765428..37b4eac88d 100644 --- a/app/src/features/human/HumanPage.tsx +++ b/app/src/features/human/HumanPage.tsx @@ -1,5 +1,6 @@ import { useEffect, useState } from 'react'; +import { MeetingBotsModal } from '../../components/skills/MeetingBotsCard'; import { useT } from '../../lib/i18n/I18nContext'; import Conversations from '../../pages/Conversations'; import type { ToolTimelineEntry } from '../../store/chatRuntimeSlice'; @@ -21,6 +22,7 @@ const HumanPage = () => { const raw = window.localStorage.getItem(SPEAK_REPLIES_KEY); return raw === null ? true : raw === '1'; }); + const [joinMeetingOpen, setJoinMeetingOpen] = useState(false); useEffect(() => { window.localStorage.setItem(SPEAK_REPLIES_KEY, speakReplies ? '1' : '0'); @@ -65,6 +67,20 @@ const HumanPage = () => { {t('voice.pushToTalk')} + {/* "Send OpenHuman to a meeting" — opens the same backend-bot modal as + the Skills tab so the mascot joins as a separate participant (its + own audio identity, no echo against the user's mic). */} + + + {joinMeetingOpen && setJoinMeetingOpen(false)} />} + {/* Chat sidebar — vertically centered above the BottomTabBar (~80px). */}
From dd080ed755d9d730e2785728055547ab6e51528b Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Thu, 21 May 2026 14:45:14 +0530 Subject: [PATCH 5/5] fix(meeting-bots): match backend's current 429 capacity-gate copy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend rewrote its free-tier paid-gate message (SERVER_OVERLOADED_MESSAGE in backend src/utils/paidPlan.ts) to "Mascot streaming capacity is exhausted. Please try again later." The frontend still pinned the old "OpenHuman is under heavy load right now…" string, so the isCapacityGated detection in joinMeetingViaMascotBot silently fell through to the generic red error path instead of the amber capacity-gated toast and inline note. Realign the constant. --- app/src/services/meetCallService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/services/meetCallService.ts b/app/src/services/meetCallService.ts index 6ca0248c25..3e97afe3c3 100644 --- a/app/src/services/meetCallService.ts +++ b/app/src/services/meetCallService.ts @@ -114,7 +114,7 @@ export interface MascotJoinMeetingResult { * without leaking the underlying paid-plan rule. */ export const SERVER_OVERLOADED_MESSAGE = - 'OpenHuman is under heavy load right now. Please try again in a few minutes.'; + 'Mascot streaming capacity is exhausted. Please try again later.'; export interface MascotJoinMeetingError { /** User-safe error text. Falls back to a generic message. */