diff --git a/app/src/components/intelligence/MemorySection.tsx b/app/src/components/intelligence/MemorySection.tsx index 345b8499a..d9e491593 100644 --- a/app/src/components/intelligence/MemorySection.tsx +++ b/app/src/components/intelligence/MemorySection.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { useT } from '../../lib/i18n/I18nContext'; import type { ToastNotification } from '../../types/intelligence'; +import { IS_DEV } from '../../utils/config'; import PillTabBar from '../PillTabBar'; import ConnectionPathTab from './ConnectionPathTab'; import DiagramViewerTab from './DiagramViewerTab'; @@ -39,17 +40,18 @@ export default function MemorySection({ onToast }: MemorySectionProps) { const { t } = useT(); const [activeSubTab, setActiveSubTab] = useState('memoryTree'); - const subTabs: { id: MemorySubTab; label: string }[] = [ + const allSubTabs: { id: MemorySubTab; label: string; devOnly?: boolean }[] = [ { id: 'memoryTree', label: t('memory.tab.memoryTree') }, - { id: 'diagram', label: t('memory.tab.diagram') }, - { id: 'centrality', label: t('memory.tab.centrality') }, - { id: 'cohesion', label: t('memory.tab.cohesion') }, - { id: 'associations', label: t('memory.tab.associations') }, - { id: 'freshness', label: t('memory.tab.freshness') }, - { id: 'timeline', label: t('memory.tab.timeline') }, - { id: 'paths', label: t('memory.tab.path') }, - { id: 'namespaces', label: t('memory.tab.namespaces') }, + { id: 'diagram', label: t('memory.tab.diagram'), devOnly: true }, + { id: 'centrality', label: t('memory.tab.centrality'), devOnly: true }, + { id: 'cohesion', label: t('memory.tab.cohesion'), devOnly: true }, + { id: 'associations', label: t('memory.tab.associations'), devOnly: true }, + { id: 'freshness', label: t('memory.tab.freshness'), devOnly: true }, + { id: 'timeline', label: t('memory.tab.timeline'), devOnly: true }, + { id: 'paths', label: t('memory.tab.path'), devOnly: true }, + { id: 'namespaces', label: t('memory.tab.namespaces'), devOnly: true }, ]; + const subTabs = allSubTabs.filter(tab => !tab.devOnly || IS_DEV); return (
diff --git a/app/src/features/human/HumanPage.tsx b/app/src/features/human/HumanPage.tsx index e7271f24f..53955bfc8 100644 --- a/app/src/features/human/HumanPage.tsx +++ b/app/src/features/human/HumanPage.tsx @@ -10,6 +10,7 @@ import { selectCustomSecondaryColor, selectMascotColor, } from '../../store/mascotSlice'; +import { IS_DEV } from '../../utils/config'; import { CustomGifMascot, getMascotPalette, hexToArgbInt, RiveMascot } from './Mascot'; import { useHumanMascot } from './useHumanMascot'; @@ -72,18 +73,20 @@ const HumanPage = () => { {t('voice.pushToTalk')} - {/* "Send OpenHuman to a meeting" — opens the Flow A modal which spawns - an off-screen CEF webview pointed at the Meet URL with the mascot - canvas as the outbound camera and synthesized speech as the - outbound mic. The user's OS mic is never wired to the meeting. */} - + {/* "Send OpenHuman to a meeting" — dev-only; opens the Flow A modal + which spawns an off-screen CEF webview pointed at the Meet URL with + the mascot canvas as the outbound camera and synthesized speech as + the outbound mic. The user's OS mic is never wired to the meeting. */} + {IS_DEV && ( + + )} {joinMeetingOpen && setJoinMeetingOpen(false)} />} diff --git a/app/src/pages/Home.tsx b/app/src/pages/Home.tsx index d3f09bee8..73bfdfb42 100644 --- a/app/src/pages/Home.tsx +++ b/app/src/pages/Home.tsx @@ -1,15 +1,12 @@ -import createDebug from 'debug'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import ConnectionIndicator from '../components/ConnectionIndicator'; import { DiscordBanner, - EarlyBirdyBanner, PromotionalCreditsBanner, UsageLimitBanner, } from '../components/home/HomeBanners'; -import { dismissBanner, shouldShowBanner } from '../components/upsell/upsellDismissState'; import { useUsageState } from '../hooks/useUsageState'; import { useUser } from '../hooks/useUser'; import { useT } from '../lib/i18n/I18nContext'; @@ -18,9 +15,6 @@ import { selectBlockingState } from '../store/connectivitySelectors'; import { useAppDispatch, useAppSelector } from '../store/hooks'; import { resolveTheme, setThemeMode, type ThemeMode } from '../store/themeSlice'; import { APP_VERSION } from '../utils/config'; -import { openhumanCronList } from '../utils/tauriCommands'; - -const homeLog = createDebug('app:home'); export function resolveHomeUserName(user: unknown): string { if (!user || typeof user !== 'object') return 'User'; @@ -57,16 +51,6 @@ const Home = () => { user?.subscription?.plan === 'FREE' || !user?.subscription?.hasActiveSubscription; const showPromoBanner = isFreeTier && promoCredits > 0.01; - // Early birdy banner: once dismissed it stays gone (cooldown longer than any realistic session). - const [showEarlyBirdy, setShowEarlyBirdy] = useState(() => - shouldShowBanner('home-earlybirdy', Number.MAX_SAFE_INTEGER) - ); - - const handleDismissEarlyBirdy = () => { - dismissBanner('home-earlybirdy'); - setShowEarlyBirdy(false); - }; - const welcomeVariants = useMemo( () => [`Welcome, ${userName} 👋`, `Let's cook, ${userName} 🧑‍🍳.`, `Time to Zone In 🧘🏻`], [userName] @@ -81,21 +65,6 @@ const Home = () => { const [isRestartingCore, setIsRestartingCore] = useState(false); const [restartError, setRestartError] = useState(null); - // Routine count for the card on home. - const [activeRoutineCount, setActiveRoutineCount] = useState(null); - const loadRoutineCount = useCallback(async () => { - try { - const response = await openhumanCronList(); - const activeCount = response.result.filter(j => j.enabled).length; - setActiveRoutineCount(activeCount); - } catch (err) { - homeLog('failed to load routine count', err); - } - }, []); - useEffect(() => { - void loadRoutineCount(); - }, [loadRoutineCount]); - const dispatch = useAppDispatch(); const themeMode = useAppSelector(state => state.theme.mode) as ThemeMode; const resolvedTheme = resolveTheme(themeMode); @@ -276,47 +245,6 @@ const Home = () => {
- {/* Routines card */} - {activeRoutineCount !== null && ( - - )} - - {showEarlyBirdy && } - {/* Next steps — compact directory of where to go next */} diff --git a/app/src/pages/Intelligence.tsx b/app/src/pages/Intelligence.tsx index 8e4fdc4e5..7d465beaf 100644 --- a/app/src/pages/Intelligence.tsx +++ b/app/src/pages/Intelligence.tsx @@ -17,6 +17,7 @@ import type { ConfirmationModal as ConfirmationModalType, ToastNotification, } from '../types/intelligence'; +import { IS_DEV } from '../utils/config'; import AgentWorkflows from './AgentWorkflows'; type IntelligenceTab = 'memory' | 'subconscious' | 'tasks' | 'workflows' | 'council'; @@ -24,7 +25,7 @@ type IntelligenceTab = 'memory' | 'subconscious' | 'tasks' | 'workflows' | 'coun export default function Intelligence() { const { t } = useT(); - const [activeTab, setActiveTab] = useState('tasks'); + const [activeTab, setActiveTab] = useState('memory'); // The legacy header pills (system-status + Ingesting/Queued chips) were // sourced from `useConsciousItems` + `useMemoryIngestionStatus`. They are @@ -87,18 +88,30 @@ export default function Intelligence() { } }, [socketConnected, socketManager]); - const tabs: { id: IntelligenceTab; label: string; description?: string; comingSoon?: boolean }[] = - [ - { id: 'tasks', label: t('memory.tab.tasks'), description: t('memory.tab.tasksDescription') }, - { id: 'memory', label: t('memory.tab.memory') }, - { id: 'subconscious', label: t('memory.tab.subconscious') }, - { - id: 'workflows', - label: t('memory.tab.workflows'), - description: t('memory.tab.workflowsDescription'), - }, - { id: 'council', label: t('memory.tab.council') }, - ]; + const allTabs: { + id: IntelligenceTab; + label: string; + description?: string; + comingSoon?: boolean; + devOnly?: boolean; + }[] = [ + { + id: 'tasks', + label: t('memory.tab.tasks'), + description: t('memory.tab.tasksDescription'), + devOnly: true, + }, + { id: 'memory', label: t('memory.tab.memory') }, + { id: 'subconscious', label: t('memory.tab.subconscious') }, + { + id: 'workflows', + label: t('memory.tab.workflows'), + description: t('memory.tab.workflowsDescription'), + devOnly: true, + }, + { id: 'council', label: t('memory.tab.council'), devOnly: true }, + ]; + const tabs = allTabs.filter(tab => !tab.devOnly || IS_DEV); const activeTabDef = tabs.find(tab => tab.id === activeTab); return ( diff --git a/app/src/pages/Skills.tsx b/app/src/pages/Skills.tsx index 1c8cd7a3b..1ed3af908 100644 --- a/app/src/pages/Skills.tsx +++ b/app/src/pages/Skills.tsx @@ -355,7 +355,8 @@ export default function Skills() { const initialTab: ConnectionsTab = (() => { const params = new URLSearchParams(location.search); const t = params.get('tab'); - if (t === 'runners' || t === 'composio' || t === 'channels' || t === 'mcp') return t; + if (t === 'runners') return IS_DEV ? 'runners' : 'composio'; + if (t === 'composio' || t === 'channels' || t === 'mcp') return t; return 'composio'; })(); const [activeTab, setActiveTab] = useState(initialTab); @@ -659,7 +660,9 @@ export default function Skills() { for (const { meta } of composioGridEntries) { cats.add(meta.category); } - return SKILL_CATEGORY_ORDER.filter(c => c !== 'Channels' && cats.has(c)); + return SKILL_CATEGORY_ORDER.filter( + c => c !== 'Channels' && cats.has(c) && (IS_DEV || c !== 'Other') + ); }, [allItems, composioGridEntries]); const filteredItems = useMemo(() => { @@ -690,7 +693,7 @@ export default function Skills() { return items.length > 0 ? { category: 'Channels' as SkillCategory, items } : undefined; }, [allItems]); const otherGroups = useMemo( - () => groupedItems.filter(g => g.category !== 'Channels'), + () => groupedItems.filter(g => g.category !== 'Channels' && (IS_DEV || g.category !== 'Other')), [groupedItems] ); @@ -941,12 +944,12 @@ export default function Skills() { { value: 'composio', label: t('skills.tabs.composio') }, { value: 'channels', label: t('skills.tabs.channels') }, { value: 'mcp', label: t('skills.tabs.mcp') }, - { value: 'runners', label: t('skills.tabs.runners') }, + ...(IS_DEV ? [{ value: 'runners' as const, label: t('skills.tabs.runners') }] : []), ]} /> { <> - {activeTab === 'runners' && ( + {IS_DEV && activeTab === 'runners' && (
{/* The Runners sub-tab IS the scheduled-skills dashboard: header + [+ Create a Skill] + [▷ Run a Skill] CTAs @@ -1102,12 +1105,21 @@ export default function Skills() { {t('channels.mcp.description')}

- {/* The real browse/install/manage surface (issue #3039). - McpServersTab manages its own height via h-full, so it - needs a sized container to fill. */} -
- -
+ {IS_DEV ? ( +
+ +
+ ) : ( +
+
🔌
+

+ {t('misc.comingSoon')} +

+

+ {t('channels.mcp.description')} +

+
+ )} )} diff --git a/app/src/pages/__tests__/Home.test.tsx b/app/src/pages/__tests__/Home.test.tsx index a19d2a6fc..871019781 100644 --- a/app/src/pages/__tests__/Home.test.tsx +++ b/app/src/pages/__tests__/Home.test.tsx @@ -61,15 +61,6 @@ vi.mock('../../services/coreProcessControl', () => ({ restartCoreProcess: () => restartCoreProcessMock(), })); -const mockShouldShowBanner = vi.fn<() => boolean>(() => true); -const mockDismissBanner = vi.fn<(id: string) => void>(); - -vi.mock('../../components/upsell/upsellDismissState', () => ({ - shouldShowBanner: (...args: Parameters) => - mockShouldShowBanner(...args), - dismissBanner: (...args: Parameters) => mockDismissBanner(...args), -})); - describe('resolveHomeUserName', () => { it('uses camelCase name fields when present', () => { expect(resolveHomeUserName({ firstName: 'Ada', lastName: 'Lovelace' })).toBe('Ada Lovelace'); @@ -111,7 +102,7 @@ describe('resolveHomeUserName', () => { describe('Home page — handleRestartCore and blocking state rendering', () => { it('shows "Restart Core" button when blocking=core-unreachable (lines 194, 200)', async () => { useAppSelectorMock.mockReturnValue('core-unreachable'); - mockShouldShowBanner.mockReturnValue(false); + const { default: Home } = await import('../Home'); render(); @@ -120,7 +111,7 @@ describe('Home page — handleRestartCore and blocking state rendering', () => { it('does NOT show "Restart Core" button when blocking=ok (line 194)', async () => { useAppSelectorMock.mockReturnValue('ok'); - mockShouldShowBanner.mockReturnValue(false); + const { default: Home } = await import('../Home'); render(); @@ -129,7 +120,7 @@ describe('Home page — handleRestartCore and blocking state rendering', () => { it('handleRestartCore calls restartCoreProcess and resets state on success (lines 78-81, 85)', async () => { useAppSelectorMock.mockReturnValue('core-unreachable'); - mockShouldShowBanner.mockReturnValue(false); + restartCoreProcessMock.mockResolvedValueOnce(undefined); const { default: Home } = await import('../Home'); @@ -150,7 +141,7 @@ describe('Home page — handleRestartCore and blocking state rendering', () => { it('handleRestartCore shows error message when restartCoreProcess throws (lines 78-83, 202)', async () => { useAppSelectorMock.mockReturnValue('core-unreachable'); - mockShouldShowBanner.mockReturnValue(false); + restartCoreProcessMock.mockRejectedValueOnce(new Error('sidecar not found')); const { default: Home } = await import('../Home'); @@ -164,7 +155,7 @@ describe('Home page — handleRestartCore and blocking state rendering', () => { it('handleRestartCore shows string error when restartCoreProcess throws a non-Error (lines 83)', async () => { useAppSelectorMock.mockReturnValue('core-unreachable'); - mockShouldShowBanner.mockReturnValue(false); + restartCoreProcessMock.mockRejectedValueOnce('raw string error'); const { default: Home } = await import('../Home'); @@ -177,40 +168,11 @@ describe('Home page — handleRestartCore and blocking state rendering', () => { }); }); -describe('Home page — EarlyBirdy banner integration', () => { - it('shows the EarlyBirdy banner when shouldShowBanner returns true', async () => { - mockShouldShowBanner.mockReturnValue(true); - const { default: Home } = await import('../Home'); - render(); - expect(screen.getByText('The first 1,000 users get 60% off.')).toBeInTheDocument(); - }); - - it('hides the EarlyBirdy banner when shouldShowBanner returns false', async () => { - mockShouldShowBanner.mockReturnValue(false); - const { default: Home } = await import('../Home'); - render(); - expect(screen.queryByText('The first 1,000 users get 60% off.')).not.toBeInTheDocument(); - }); - - it('dismisses the EarlyBirdy banner and calls dismissBanner when the X button is clicked', async () => { - mockShouldShowBanner.mockReturnValue(true); - const { default: Home } = await import('../Home'); - render(); - - const dismissBtn = screen.getByRole('button', { name: /dismiss early bird banner/i }); - fireEvent.click(dismissBtn); - - expect(mockDismissBanner).toHaveBeenCalledWith('home-earlybirdy'); - expect(screen.queryByText('The first 1,000 users get 60% off.')).not.toBeInTheDocument(); - }); -}); - describe('Home page — theme toggle', () => { it('renders "Switch to dark mode" in light mode and dispatches setThemeMode("dark") on click', async () => { themeModeProbe.current = 'light'; const dispatch = vi.fn(); useAppDispatchMock.mockReturnValue(dispatch); - mockShouldShowBanner.mockReturnValue(false); const { default: Home } = await import('../Home'); render(); @@ -228,7 +190,6 @@ describe('Home page — theme toggle', () => { themeModeProbe.current = 'dark'; const dispatch = vi.fn(); useAppDispatchMock.mockReturnValue(dispatch); - mockShouldShowBanner.mockReturnValue(false); const { default: Home } = await import('../Home'); render(); @@ -248,7 +209,6 @@ describe('Home page — budget completed banner', () => { // Covers line 151: UsageLimitBanner render when shouldShowBudgetCompletedMessage=true it('renders UsageLimitBanner when shouldShowBudgetCompletedMessage=true', async () => { mockUseUsageState.mockReturnValueOnce({ shouldShowBudgetCompletedMessage: true }); - mockShouldShowBanner.mockReturnValue(false); const { default: Home } = await import('../Home'); render();