From da5092d469af5347582d1420dc4b4e2848135fe5 Mon Sep 17 00:00:00 2001 From: Cyrus Gray <144336577+graycyrus@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:18:03 +0530 Subject: [PATCH] feat(nav): remove agent profile avatar from bottom tab bar (#3533) --- app/src/components/BottomTabBar.tsx | 203 +----------------- .../__tests__/BottomTabBar.test.tsx | 79 +------ .../specs/agent-profile-taskbar.spec.ts | 44 ---- 3 files changed, 3 insertions(+), 323 deletions(-) delete mode 100644 app/test/playwright/specs/agent-profile-taskbar.spec.ts diff --git a/app/src/components/BottomTabBar.tsx b/app/src/components/BottomTabBar.tsx index 98f918785..85226c0af 100644 --- a/app/src/components/BottomTabBar.tsx +++ b/app/src/components/BottomTabBar.tsx @@ -1,20 +1,12 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; +import { useMemo, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useT } from '../lib/i18n/I18nContext'; import { useCoreState } from '../providers/CoreStateProvider'; import { trackEvent } from '../services/analytics'; -import { - loadAgentProfiles, - selectActiveAgentProfile, - selectActiveAgentProfileId, - selectAgentProfile, - selectAgentProfiles, -} from '../store/agentProfileSlice'; import { selectCompanionSessionActive } from '../store/companionSlice'; -import { useAppDispatch, useAppSelector } from '../store/hooks'; +import { useAppSelector } from '../store/hooks'; import { selectUnreadCount } from '../store/notificationSlice'; -import type { AgentProfile } from '../types/agentProfile'; import { isAccountsFullscreen } from '../utils/accountsFullscreen'; const makeTabs = (t: (key: string) => string) => [ @@ -134,91 +126,23 @@ const makeTabs = (t: (key: string) => string) => [ }, ]; -const getInitials = (name: string): string => { - const words = name.trim().split(/\s+/).filter(Boolean); - if (words.length === 0) return 'OH'; - return words - .slice(0, 2) - .map(word => word[0]?.toUpperCase() ?? '') - .join(''); -}; - -function AgentProfileAvatar({ - profile, - fallbackName, -}: { - profile: AgentProfile | null; - fallbackName: string; -}) { - const name = profile?.name?.trim() || fallbackName; - if (profile?.avatarUrl) { - return ( - - ); - } - - return ( - - {getInitials(name)} - - ); -} - const BottomTabBar = () => { const { t } = useT(); const location = useLocation(); const navigate = useNavigate(); - const dispatch = useAppDispatch(); const { snapshot } = useCoreState(); const token = snapshot.sessionToken; const [revealed, setRevealed] = useState(false); - const [profileMenuOpen, setProfileMenuOpen] = useState(false); - const profileMenuRef = useRef(null); const activeAccountId = useAppSelector(state => state.accounts.activeAccountId); const unreadCount = useAppSelector(state => selectUnreadCount(state.notifications.items)); const companionActive = useAppSelector(selectCompanionSessionActive); - const agentProfiles = useAppSelector(selectAgentProfiles); - const activeAgentProfileId = useAppSelector(selectActiveAgentProfileId); - const activeAgentProfile = useAppSelector(selectActiveAgentProfile); - const agentProfileStatus = useAppSelector(state => state.agentProfiles.status); - const agentProfileError = useAppSelector(state => state.agentProfiles.error); // `state.theme` is undefined in some test fixtures that build a minimal // store without the theme slice; default to the historical 'hover' behavior // so an absent theme branch can't crash the bar. const tabBarLabels = useAppSelector(state => state.theme?.tabBarLabels ?? 'hover'); const labelsAlwaysVisible = tabBarLabels === 'always'; const tabs = useMemo(() => makeTabs(t), [t]); - const fallbackProfileName = t('nav.defaultAgentProfile'); - - useEffect(() => { - if (!token || agentProfiles.length > 0 || agentProfileStatus !== 'idle') return; - void dispatch(loadAgentProfiles()); - }, [agentProfileStatus, agentProfiles.length, dispatch, token]); - - useEffect(() => { - if (!profileMenuOpen) return; - - const onPointerDown = (event: PointerEvent) => { - if (profileMenuRef.current?.contains(event.target as Node)) return; - setProfileMenuOpen(false); - }; - const onKeyDown = (event: KeyboardEvent) => { - if (event.key === 'Escape') setProfileMenuOpen(false); - }; - - document.addEventListener('pointerdown', onPointerDown); - document.addEventListener('keydown', onKeyDown); - return () => { - document.removeEventListener('pointerdown', onPointerDown); - document.removeEventListener('keydown', onKeyDown); - }; - }, [profileMenuOpen]); const hiddenPaths = ['/', '/login']; if ( @@ -265,30 +189,6 @@ const BottomTabBar = () => { navigate(tab.path); }; - const handleProfileSelect = async (profile: AgentProfile) => { - if (profile.id === activeAgentProfileId) { - setProfileMenuOpen(false); - return; - } - - trackEvent('agent_profile_switch', { - from_profile_id: activeAgentProfileId, - to_profile_id: profile.id, - from_path: location.pathname, - }); - - try { - await dispatch(selectAgentProfile(profile.id)).unwrap(); - setProfileMenuOpen(false); - } catch (error) { - console.warn('[BottomTabBar] agent profile select failed', error); - } - }; - - const handleRetryProfiles = () => { - void dispatch(loadAgentProfiles()); - }; - return ( // pointer-events-none on the full-width shell so transparent areas (e.g. // beside the centered nav pill) do not steal clicks from sticky footers @@ -362,105 +262,6 @@ const BottomTabBar = () => { ); })} -
- - - {profileMenuOpen && ( -
-
- {t('nav.agentProfiles')} -
- - {agentProfileError && ( -
-
{agentProfileError}
- -
- )} - - {!agentProfileError && agentProfileStatus === 'loading' && ( -
- {t('common.loading')} -
- )} - - {!agentProfileError && - agentProfileStatus !== 'loading' && - agentProfiles.length === 0 && ( -
- {t('nav.noAgentProfiles')} -
- )} - - {!agentProfileError && agentProfiles.length > 0 && ( -
- {agentProfiles.map(profile => { - const active = profile.id === activeAgentProfileId; - return ( - - ); - })} -
- )} -
- )} -
diff --git a/app/src/components/__tests__/BottomTabBar.test.tsx b/app/src/components/__tests__/BottomTabBar.test.tsx index 0938c10c4..67276f00e 100644 --- a/app/src/components/__tests__/BottomTabBar.test.tsx +++ b/app/src/components/__tests__/BottomTabBar.test.tsx @@ -7,13 +7,12 @@ * [#1123] Covers the walkthroughAttr object added for the Joyride walkthrough. */ import { configureStore } from '@reduxjs/toolkit'; -import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { fireEvent, render, screen } from '@testing-library/react'; import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import accountsReducer from '../../store/accountsSlice'; -import agentProfileReducer, { setAgentProfilesFromResponse } from '../../store/agentProfileSlice'; import companionReducer from '../../store/companionSlice'; import notificationReducer from '../../store/notificationSlice'; import BottomTabBar from '../BottomTabBar'; @@ -22,15 +21,6 @@ import BottomTabBar from '../BottomTabBar'; vi.mock('../../providers/CoreStateProvider', () => ({ useCoreState: vi.fn() })); -const agentProfilesApiMock = vi.hoisted(() => ({ - list: vi.fn(), - select: vi.fn(), - upsert: vi.fn(), - delete: vi.fn(), -})); - -vi.mock('../../services/api/agentProfilesApi', () => ({ agentProfilesApi: agentProfilesApiMock })); - vi.mock('../../utils/config', async importOriginal => { const actual = await importOriginal(); return { ...actual, APP_ENVIRONMENT: 'development' }; @@ -45,44 +35,14 @@ interface BuildStoreOpts { companionSessionActive?: boolean; } -const testProfiles = { - activeProfileId: 'planner', - profiles: [ - { - id: 'default', - name: 'Orchestrator', - description: 'Default agent', - agentId: 'orchestrator', - builtIn: true, - }, - { - id: 'planner', - name: 'Planner', - description: 'Plans multi-step work', - agentId: 'planner', - builtIn: true, - avatarUrl: 'https://example.com/planner.png', - }, - { - id: 'research', - name: 'Research', - description: 'Finds and summarizes sources', - agentId: 'research', - builtIn: true, - }, - ], -}; - function buildStore(opts: BuildStoreOpts = {}) { const store = configureStore({ reducer: { accounts: accountsReducer, notifications: notificationReducer, companion: companionReducer, - agentProfiles: agentProfileReducer, }, }); - store.dispatch(setAgentProfilesFromResponse(testProfiles)); if (opts.companionSessionActive) { store.dispatch({ type: 'companion/setSessionActive', @@ -146,7 +106,6 @@ async function renderBottomTabBar(pathname = '/home', opts: RenderOpts | boolean describe('BottomTabBar', () => { beforeEach(() => { vi.clearAllMocks(); - agentProfilesApiMock.select.mockResolvedValue(testProfiles); }); // [#1123] Covers line 222 — walkthroughAttr object created per-tab inside .map() @@ -223,40 +182,4 @@ describe('BottomTabBar', () => { expect(trackEvent).not.toHaveBeenCalled(); }); - - it('renders an avatar-only agent profile switcher with the active profile', async () => { - await renderBottomTabBar('/home'); - - const switcher = screen.getByRole('button', { name: 'Switch agent profile: Planner' }); - expect(switcher).toHaveAttribute('title', 'Planner'); - expect(switcher.querySelector('img')).toHaveAttribute('src', 'https://example.com/planner.png'); - - fireEvent.click(switcher); - - expect(screen.getByRole('menu', { name: 'Agent profiles' })).toBeInTheDocument(); - expect(screen.getByRole('menuitemradio', { name: /Planner/ })).toHaveAttribute( - 'aria-checked', - 'true' - ); - expect(screen.getByRole('menuitemradio', { name: /Research/ })).toBeInTheDocument(); - }); - - it('switches the active agent profile from the taskbar menu', async () => { - const { trackEvent } = await import('../../services/analytics'); - agentProfilesApiMock.select.mockResolvedValueOnce({ - ...testProfiles, - activeProfileId: 'research', - }); - await renderBottomTabBar('/home'); - - fireEvent.click(screen.getByRole('button', { name: 'Switch agent profile: Planner' })); - fireEvent.click(screen.getByRole('menuitemradio', { name: /Research/ })); - - await waitFor(() => expect(agentProfilesApiMock.select).toHaveBeenCalledWith('research')); - expect(trackEvent).toHaveBeenCalledWith('agent_profile_switch', { - from_profile_id: 'planner', - to_profile_id: 'research', - from_path: '/home', - }); - }); }); diff --git a/app/test/playwright/specs/agent-profile-taskbar.spec.ts b/app/test/playwright/specs/agent-profile-taskbar.spec.ts deleted file mode 100644 index 83dac56d9..000000000 --- a/app/test/playwright/specs/agent-profile-taskbar.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { expect, type Page, test } from '@playwright/test'; - -import { - bootAuthenticatedPage, - dismissWalkthroughIfPresent, - waitForAppReady, -} from '../helpers/core-rpc'; - -async function activeAgentProfileId(page: Page): Promise { - return page.evaluate(() => { - const win = window as unknown as { - __OPENHUMAN_STORE__?: { - getState?: () => { agentProfiles?: { activeProfileId?: string | null } }; - }; - }; - return win.__OPENHUMAN_STORE__?.getState?.().agentProfiles?.activeProfileId ?? null; - }); -} - -test.describe('Agent profile taskbar switcher', () => { - test('switches the active agent profile from the bottom taskbar avatar menu', async ({ - page, - }) => { - await bootAuthenticatedPage(page, 'pw-agent-profile-taskbar', '/home'); - await waitForAppReady(page); - await dismissWalkthroughIfPresent(page); - - const switcher = page.getByRole('button', { name: /Switch agent profile:/ }); - await expect(switcher).toBeVisible(); - await expect(switcher).toHaveAttribute('aria-label', 'Switch agent profile: Default'); - - await switcher.click(); - await expect(page.getByRole('menu', { name: 'Agent profiles' })).toBeVisible(); - await expect(page.getByRole('menuitemradio', { name: /Default/ })).toHaveAttribute( - 'aria-checked', - 'true' - ); - - await page.getByRole('menuitemradio', { name: /Research/ }).click(); - - await expect.poll(() => activeAgentProfileId(page), { timeout: 10_000 }).toBe('research'); - await expect(switcher).toHaveAttribute('aria-label', 'Switch agent profile: Research'); - }); -});