From 04bec2340589e92eef8dec557eafcbbed285556a Mon Sep 17 00:00:00 2001 From: Cyrus Gray <144336577+graycyrus@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:30:00 +0530 Subject: [PATCH] IA revamp: restructure app navigation and settings for everyday users (phased) (#3518) --- app/src/AppRoutes.tsx | 74 +- app/src/components/BottomTabBar.tsx | 315 +++-- .../__tests__/BottomTabBar.test.tsx | 173 ++- .../rewards/RewardsCommunityTab.tsx | 2 +- app/src/components/settings/SettingsHome.tsx | 531 +++++--- .../settings/__tests__/SettingsHome.test.tsx | 230 +++- .../settings/components/SettingsMenuItem.tsx | 4 +- .../settings/hooks/useSettingsNavigation.ts | 17 +- .../components/settings/panels/AboutPanel.tsx | 48 +- .../settings/panels/AgentAccessPanel.tsx | 280 +--- .../settings/panels/AnalysisViewsPanel.tsx | 79 ++ .../settings/panels/DeveloperOptionsPanel.tsx | 1134 ++++++++++++----- .../settings/panels/MemorySyncPanel.tsx | 62 + .../settings/panels/PermissionsPanel.tsx | 373 ++++++ .../panels/__tests__/AboutPanel.test.tsx | 51 + .../__tests__/AgentAccessPanel.test.tsx | 185 +-- .../__tests__/DeveloperOptionsPanel.test.tsx | 4 +- .../__tests__/PermissionsPanel.test.tsx | 239 ++++ .../__tests__/AppWalkthrough.test.tsx | 6 +- .../walkthrough/walkthroughSteps.ts | 14 +- app/src/config/__tests__/navConfig.test.ts | 126 ++ app/src/config/navConfig.ts | 108 ++ app/src/hooks/useDeveloperMode.ts | 18 + .../commands/__tests__/globalActions.test.tsx | 11 +- app/src/lib/commands/globalActions.ts | 18 +- app/src/lib/i18n/ar.ts | 116 +- app/src/lib/i18n/bn.ts | 118 +- app/src/lib/i18n/de.ts | 121 +- app/src/lib/i18n/en.ts | 136 +- app/src/lib/i18n/es.ts | 121 +- app/src/lib/i18n/fr.ts | 123 +- app/src/lib/i18n/hi.ts | 116 +- app/src/lib/i18n/id.ts | 120 +- app/src/lib/i18n/it.ts | 120 +- app/src/lib/i18n/ko.ts | 118 +- app/src/lib/i18n/pl.ts | 119 +- app/src/lib/i18n/pt.ts | 123 +- app/src/lib/i18n/ru.ts | 123 +- app/src/lib/i18n/zh-CN.ts | 111 +- app/src/lib/notificationRouter.test.ts | 4 +- app/src/lib/notificationRouter.ts | 4 +- app/src/pages/Accounts.tsx | 172 ++- app/src/pages/Activity.tsx | 193 +++ app/src/pages/Home.tsx | 27 +- app/src/pages/Intelligence.test.tsx | 6 +- app/src/pages/Intelligence.tsx | 20 +- app/src/pages/Settings.tsx | 35 +- app/src/pages/Skills.tsx | 73 +- app/src/pages/WorkflowNew.test.tsx | 8 +- app/src/pages/WorkflowNew.tsx | 19 +- .../__tests__/Accounts.faceToggle.test.tsx | 178 +++ .../__tests__/Activity.redirects.test.tsx | 57 + app/src/pages/__tests__/Activity.test.tsx | 174 +++ .../pages/__tests__/AppRoutes.phase6.test.tsx | 42 + .../__tests__/Connections.redirects.test.tsx | 50 + .../__tests__/Conversations.render.test.tsx | 4 +- app/src/pages/__tests__/Intelligence.test.tsx | 4 + .../__tests__/Skills.channels-grid.test.tsx | 26 +- .../Skills.composio-catalog.test.tsx | 46 +- .../__tests__/Skills.mcp-coming-soon.test.tsx | 29 +- .../__tests__/Skills.meetings-tab.test.tsx | 29 +- .../Skills.third-party-gmail-sync.test.tsx | 6 +- ...ls.third-party-notion-debug-tools.test.tsx | 6 +- app/src/store/developerModeSlice.test.ts | 55 + app/src/store/index.ts | 2 +- app/src/store/themeSlice.test.ts | 1 + app/src/store/themeSlice.ts | 28 +- app/src/utils/desktopDeepLinkListener.ts | 2 +- app/src/utils/userName.ts | 30 + app/test/e2e/helpers/shared-flows.ts | 31 +- .../conversations-web-channel-flow.spec.ts | 3 +- app/test/e2e/specs/guided-tour-gates.spec.ts | 12 +- .../e2e/specs/memory-sync-schedule.spec.ts | 14 +- .../e2e/specs/navigation-smoothness.spec.ts | 37 +- app/test/e2e/specs/navigation.spec.ts | 9 +- .../settings-channels-permissions.spec.ts | 5 +- .../settings-feature-preferences.spec.ts | 9 +- .../e2e/specs/skill-execution-flow.spec.ts | 6 +- app/test/e2e/specs/skill-lifecycle.spec.ts | 23 +- app/test/e2e/specs/skills-registry.spec.ts | 13 +- app/test/e2e/specs/voice-mode.spec.ts | 10 +- .../playwright/specs/channels-smoke.spec.ts | 10 +- .../playwright/specs/command-palette.spec.ts | 4 +- .../specs/composio-triggers-flow.spec.ts | 27 +- .../specs/connector-gmail-composio.spec.ts | 28 +- .../connector-session-guard-matrix.spec.ts | 19 +- app/test/playwright/specs/gmail-flow.spec.ts | 31 +- .../specs/gmeet-connections-tab.spec.ts | 19 +- .../specs/guided-tour-gates.spec.ts | 9 +- .../specs/insights-dashboard.spec.ts | 11 +- .../intelligence-memory-ui-functional.spec.ts | 24 +- .../playwright/specs/mcp-tab-flow.spec.ts | 23 +- .../specs/navigation-settings-panels.spec.ts | 3 +- .../specs/navigation-smoothness.spec.ts | 12 +- app/test/playwright/specs/navigation.spec.ts | 29 +- .../specs/settings-advanced-config.spec.ts | 4 +- .../settings-channels-permissions.spec.ts | 9 +- .../settings-feature-preferences.spec.ts | 12 +- .../specs/skill-execution-flow.spec.ts | 7 +- .../playwright/specs/skill-lifecycle.spec.ts | 12 +- app/test/playwright/specs/skill-oauth.spec.ts | 7 +- .../playwright/specs/skills-registry.spec.ts | 34 +- .../specs/top-level-functional-flows.spec.ts | 10 +- .../user-journey-settings-round-trip.spec.ts | 3 +- 104 files changed, 6002 insertions(+), 1634 deletions(-) create mode 100644 app/src/components/settings/panels/AnalysisViewsPanel.tsx create mode 100644 app/src/components/settings/panels/MemorySyncPanel.tsx create mode 100644 app/src/components/settings/panels/PermissionsPanel.tsx create mode 100644 app/src/components/settings/panels/__tests__/PermissionsPanel.test.tsx create mode 100644 app/src/config/__tests__/navConfig.test.ts create mode 100644 app/src/config/navConfig.ts create mode 100644 app/src/hooks/useDeveloperMode.ts create mode 100644 app/src/pages/Activity.tsx create mode 100644 app/src/pages/__tests__/Accounts.faceToggle.test.tsx create mode 100644 app/src/pages/__tests__/Activity.redirects.test.tsx create mode 100644 app/src/pages/__tests__/Activity.test.tsx create mode 100644 app/src/pages/__tests__/AppRoutes.phase6.test.tsx create mode 100644 app/src/pages/__tests__/Connections.redirects.test.tsx create mode 100644 app/src/store/developerModeSlice.test.ts create mode 100644 app/src/utils/userName.ts diff --git a/app/src/AppRoutes.tsx b/app/src/AppRoutes.tsx index 7bfa4ec09..fd8c33444 100644 --- a/app/src/AppRoutes.tsx +++ b/app/src/AppRoutes.tsx @@ -4,18 +4,15 @@ import AppRoutesIOS from './AppRoutesIOS'; import DefaultRedirect from './components/DefaultRedirect'; import ProtectedRoute from './components/ProtectedRoute'; import PublicRoute from './components/PublicRoute'; -import HumanPage from './features/human/HumanPage'; import { getIsMobile } from './lib/platform'; import Accounts from './pages/Accounts'; -import Channels from './pages/Channels'; +import Activity from './pages/Activity'; import Home from './pages/Home'; -import Intelligence from './pages/Intelligence'; import Invites from './pages/Invites'; import Notifications from './pages/Notifications'; import Onboarding from './pages/onboarding/Onboarding'; import { PttOverlayPage } from './pages/PttOverlayPage'; import Rewards from './pages/Rewards'; -import Routines from './pages/Routines'; import Settings from './pages/Settings'; import Skills from './pages/Skills'; import WebCallbackPage from './pages/WebCallbackPage'; @@ -65,33 +62,34 @@ const AppRoutes = () => { } /> + {/* Phase 6 — /human merged into /chat (Assistant surface). + Preserve the route for back-compat (deep links, iOS share sheets, etc.). + iOS AppRoutesIOS still serves /human natively — only desktop redirects. */} + } /> + + {/* Primary Activity surface — replaces /intelligence (Phase 3). */} - + } /> - - - - } - /> + {/* Back-compat: /intelligence → /activity (preserves ?tab= deep links). + Deep links such as ?tab=memory or ?tab=agents still resolve but fall + back to the tasks tab in prod (dev-only tabs are gated inside Activity). */} + } /> - {/* Skills lives at /skills with its 4 sub-tabs (Composio / Channels / - MCP Servers / Runners). The scheduled-skills dashboard concept - composes INSIDE the Runners sub-tab, not as a separate top-level - page — the bottom-bar "Connections" entry has always pointed at - /skills to surface Composio integrations + MCP, and that muscle - memory is restored here. + {/* Connections page lives at /connections (Phase 2 rename from /skills). + The old /skills path is kept as a back-compat redirect so bookmarks + and deep links continue to work. `?tab=` query params are preserved + by Navigate (replace) so existing deep links still land on the right + sub-tab. `/workflows/new` is the create-a-skill authoring page. - Order matters: keep `/workflows/new` before `/skills` so it wins the - prefix match. */} + Order matters: keep `/workflows/new` before `/connections` so it wins + the prefix match. */} { /> @@ -119,6 +117,9 @@ const AppRoutes = () => { } /> + {/* Back-compat: /skills → /connections (preserves ?tab= deep links). */} + } /> + {/* Unified chat = agent + connected web apps. Replaces the old /conversations and /accounts routes. */} { } /> - - - - } - /> + {/* Back-compat: /channels was an orphaned standalone page; it now + redirects to the unified Connections page on the Messaging tab. */} + } /> { } /> - - - - } - /> + {/* Back-compat: /routines was an orphaned dead page (superseded by the + Cron Jobs settings panel). Redirect to Activity → Automations so + any surviving deep links land somewhere sensible. */} + } /> { } /> - {/* Workflows moved onto the Intelligence page (its own tab). Keep the + {/* Workflows moved onto the Activity page (Automations tab). Keep the old /workflows path working as a deep link into that tab. */} - } /> + } /> } /> diff --git a/app/src/components/BottomTabBar.tsx b/app/src/components/BottomTabBar.tsx index 85226c0af..fe9e0a456 100644 --- a/app/src/components/BottomTabBar.tsx +++ b/app/src/components/BottomTabBar.tsx @@ -1,6 +1,7 @@ -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; +import { AVATAR_MENU_ITEMS, NAV_TABS } from '../config/navConfig'; import { useT } from '../lib/i18n/I18nContext'; import { useCoreState } from '../providers/CoreStateProvider'; import { trackEvent } from '../services/analytics'; @@ -8,123 +9,105 @@ import { selectCompanionSessionActive } from '../store/companionSlice'; import { useAppSelector } from '../store/hooks'; import { selectUnreadCount } from '../store/notificationSlice'; import { isAccountsFullscreen } from '../utils/accountsFullscreen'; +import { BILLING_DASHBOARD_URL } from '../utils/links'; +import { isLocalSessionToken } from '../utils/localSession'; +import { openUrl } from '../utils/openUrl'; +import { resolveUserName } from '../utils/userName'; -const makeTabs = (t: (key: string) => string) => [ - { - id: 'home', - label: t('nav.home'), - path: '/home', - icon: ( - - - - ), - }, - { - id: 'human', - label: t('nav.human'), - path: '/human', - icon: ( - - - - ), - }, - { - id: 'chat', - label: t('nav.chat'), - path: '/chat', - icon: ( - - - - ), - }, - { - id: 'skills', - label: t('nav.connections'), - path: '/skills', - icon: ( - - - - ), - }, - { - id: 'intelligence', - label: t('nav.memory'), - path: '/intelligence', - icon: ( - - - - ), - }, - // Alerts/Notifications used to be its own bottom tab; moved into - // Settings › Notifications since it's a low-traffic destination. - // The /notifications route still exists for deep links. - { - id: 'rewards', - label: t('nav.rewards'), - path: '/rewards', - icon: ( - - - - ), - }, - { - id: 'settings', - label: t('nav.settings'), - path: '/settings', - icon: ( - - - - - ), - }, -]; +// ── SVG icons, keyed by tab id ──────────────────────────────────────────────── + +function TabIcon({ id }: { id: string }) { + switch (id) { + case 'home': + return ( + + + + ); + case 'human': + return ( + + + + ); + case 'chat': + return ( + + + + ); + case 'connections': + return ( + + + + ); + case 'activity': + // Reuse the Intelligence/memory lightbulb icon for the Activity tab + return ( + + + + ); + case 'settings': + return ( + + + + + ); + default: + return null; + } +} + +// ── Helpers ─────────────────────────────────────────────────────────────────── + +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(''); +}; + +// ── Component ───────────────────────────────────────────────────────────────── const BottomTabBar = () => { const { t } = useT(); @@ -133,6 +116,8 @@ const BottomTabBar = () => { 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)); @@ -142,7 +127,32 @@ const BottomTabBar = () => { // 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 isLocalSession = isLocalSessionToken(token); + // The avatar button shows the signed-in user's initials. + const userInitials = getInitials(resolveUserName(snapshot.currentUser)); + + // Resolve translated labels for NAV_TABS once per render cycle. + const tabs = useMemo(() => NAV_TABS.map(tab => ({ ...tab, label: t(tab.labelKey) })), [t]); + + 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 ( @@ -189,6 +199,16 @@ const BottomTabBar = () => { navigate(tab.path); }; + const handleAvatarMenuItemClick = (itemId: string, kind: string, target: string) => { + setProfileMenuOpen(false); + if (kind === 'openUrl') { + openUrl(target).catch(() => {}); + } else { + navigate(target); + } + trackEvent('avatar_menu_item_click', { item_id: itemId }); + }; + 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 @@ -217,18 +237,10 @@ const BottomTabBar = () => { const active = isActive(tab.path); const showBadge = tab.id === 'notifications' && unreadCount > 0; const showCompanionDot = tab.id === 'settings' && companionActive; - // data-walkthrough attributes for the Joyride walkthrough steps. - // Maps tab ids to their walkthrough target names. - const walkthroughAttr: Record = { - chat: 'tab-chat', - skills: 'tab-skills', - notifications: 'tab-notifications', - settings: 'tab-settings', - }; return ( ); })} +
+ + + {profileMenuOpen && ( +
+
+ {AVATAR_MENU_ITEMS.filter(item => !item.cloudOnly || !isLocalSession).map( + item => { + // Billing target is resolved from the canonical constant rather than the + // data-file placeholder so it stays in sync with SettingsHome. + const target = item.id === 'billing' ? BILLING_DASHBOARD_URL : item.target; + return ( + + ); + } + )} +
+
+ )} +
diff --git a/app/src/components/__tests__/BottomTabBar.test.tsx b/app/src/components/__tests__/BottomTabBar.test.tsx index 67276f00e..9aef3141b 100644 --- a/app/src/components/__tests__/BottomTabBar.test.tsx +++ b/app/src/components/__tests__/BottomTabBar.test.tsx @@ -1,10 +1,13 @@ /** * Tests for BottomTabBar — verifies that: - * - the tab bar renders when the user has a session token and is on a non-hidden path - * - the walkthroughAttr mapping (line 222) is exercised by rendering the tabs - * - the tab bar is hidden on '/' and '/login' paths + * - 5 tabs are rendered (no Rewards tab, no Human tab), Activity label is present + * - Assistant tab is present (was "Chat", id stays 'chat', label now 'Assistant') + * - Walkthrough attributes reflect the new ids (tab-connections, tab-activity) + * - Avatar menu opens and shows Account / Billing / Rewards / Invites / Wallet + * - Clicking an avatar menu item navigates or opens URL + * - The bar is hidden on '/' and '/login' paths * - * [#1123] Covers the walkthroughAttr object added for the Joyride walkthrough. + * Updated for IA Phase 6: Human tab removed; Chat renamed to Assistant. */ import { configureStore } from '@reduxjs/toolkit'; import { fireEvent, render, screen } from '@testing-library/react'; @@ -13,6 +16,7 @@ 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'; @@ -21,6 +25,15 @@ 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' }; @@ -29,20 +42,53 @@ vi.mock('../../utils/config', async importOriginal => { vi.mock('../../utils/accountsFullscreen', () => ({ isAccountsFullscreen: vi.fn(() => false) })); vi.mock('../../services/analytics', () => ({ trackEvent: vi.fn() })); +// Mock openUrl so tests don't try to open real URLs +vi.mock('../../utils/openUrl', () => ({ openUrl: vi.fn().mockResolvedValue(undefined) })); + // ── Helpers ──────────────────────────────────────────────────────────────── 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', @@ -56,6 +102,7 @@ interface RenderOpts { hasToken?: boolean; companionSessionActive?: boolean; tokenValue?: string; + currentUser?: unknown; } async function renderBottomTabBar(pathname = '/home', opts: RenderOpts | boolean = {}) { @@ -68,7 +115,7 @@ async function renderBottomTabBar(pathname = '/home', opts: RenderOpts | boolean snapshot: { sessionToken: hasToken ? tokenValue : null, auth: { isAuthenticated: true, userId: 'u1', user: null, profileId: null }, - currentUser: null, + currentUser: resolved.currentUser ?? null, onboardingCompleted: true, chatOnboardingCompleted: true, analyticsEnabled: false, @@ -106,19 +153,50 @@ 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() - it('renders navigation tabs with data-walkthrough attributes when session is active', async () => { + it('renders exactly 5 tab buttons (Phase 6: Human merged into Assistant)', async () => { await renderBottomTabBar('/home'); + // Query only buttons inside