diff --git a/app/src/components/layout/shell/SidebarAppRail.test.tsx b/app/src/components/layout/shell/SidebarAppRail.test.tsx index 68c0a9fb8..8510c72bb 100644 --- a/app/src/components/layout/shell/SidebarAppRail.test.tsx +++ b/app/src/components/layout/shell/SidebarAppRail.test.tsx @@ -7,23 +7,36 @@ import SidebarAppRail from './SidebarAppRail'; const mockNavigate = vi.fn(); const mockDispatch = vi.fn(); -const mockState = { +const whatsappAccount = { + id: 'acct-whatsapp', + provider: 'whatsapp', + label: 'WhatsApp', + createdAt: '2026-01-01T00:00:00.000Z', + status: 'open', +}; + +const accountsWith: Record = { 'acct-whatsapp': whatsappAccount }; + +let mockState = { accounts: { - accounts: { - 'acct-whatsapp': { - id: 'acct-whatsapp', - provider: 'whatsapp', - label: 'WhatsApp', - createdAt: '2026-01-01T00:00:00.000Z', - status: 'open', - }, - }, + accounts: accountsWith, order: ['acct-whatsapp'], - activeAccountId: null, - unread: {}, + activeAccountId: null as string | null, + unread: {} as Record, }, }; +function setAccounts(order: string[]) { + mockState = { + accounts: { + accounts: order.length ? accountsWith : {}, + order, + activeAccountId: null, + unread: {}, + }, + }; +} + vi.mock('react-router-dom', async importOriginal => { const actual = await importOriginal(); return { ...actual, useNavigate: () => mockNavigate }; @@ -40,6 +53,7 @@ vi.mock('../../../store/hooks', () => ({ describe('SidebarAppRail', () => { beforeEach(() => { vi.clearAllMocks(); + setAccounts(['acct-whatsapp']); }); it('selects a provider webview without mutating the current route', () => { @@ -65,6 +79,23 @@ describe('SidebarAppRail', () => { payload: '__agent__', }); }); + + it('shows the "Add apps" label when no provider apps are connected', () => { + setAccounts([]); + renderRail('/chat'); + + const addButton = screen.getByTestId('accounts-add-button'); + expect(addButton).toHaveTextContent('accounts.addApps'); + }); + + it('collapses the add button to an icon once an app is connected', () => { + setAccounts(['acct-whatsapp']); + renderRail('/chat'); + + const addButton = screen.getByTestId('accounts-add-button'); + expect(addButton).not.toHaveTextContent('accounts.addApps'); + expect(addButton).toHaveAttribute('aria-label', 'accounts.addApps'); + }); }); function renderRail(route: string) { diff --git a/app/src/components/layout/shell/SidebarAppRail.tsx b/app/src/components/layout/shell/SidebarAppRail.tsx index 658c7e206..7f612420d 100644 --- a/app/src/components/layout/shell/SidebarAppRail.tsx +++ b/app/src/components/layout/shell/SidebarAppRail.tsx @@ -113,6 +113,13 @@ export default function SidebarAppRail() { const selectedId = activeAccountId ?? AGENT_ID; const isAgentSelected = selectedId === AGENT_ID; + // New-user affordance: when no provider apps are connected yet, the lone + // dashed "+" tile reads as ambiguous. Expand it into a labelled "Add apps" + // pill so first-run users understand they can connect more apps; once at + // least one app is connected, collapse back to the compact icon to reclaim + // horizontal space in the rail. + const showAddLabel = accounts.length === 0; + // The chat page hides its active provider webview while a rail overlay is // open (the native CEF view composites above HTML, so React overlays would be // painted over). Mirror local overlay state into Redux for it to read. @@ -253,12 +260,17 @@ export default function SidebarAppRail() { }} data-analytics-id="sidebar-app-rail-add-account" data-testid="accounts-add-button" - className="group relative flex h-9 w-9 flex-none items-center justify-center rounded-xl border border-dashed border-stone-300 text-stone-400 hover:bg-stone-50 hover:text-stone-600 dark:border-neutral-700 dark:text-neutral-500 dark:hover:bg-neutral-800/60 dark:hover:text-neutral-300" - aria-label={t('accounts.addAccount')} - title={t('accounts.addAccount')}> - + className={`group relative flex h-9 flex-none items-center justify-center gap-1.5 rounded-xl border border-dashed border-stone-300 text-stone-400 transition-colors hover:bg-stone-50 hover:text-stone-600 dark:border-neutral-700 dark:text-neutral-500 dark:hover:bg-neutral-800/60 dark:hover:text-neutral-300 ${ + showAddLabel ? 'w-auto px-2.5' : 'w-9' + }`} + aria-label={t('accounts.addApps')} + title={t('accounts.addApps')}> + + {showAddLabel && ( + {t('accounts.addApps')} + )} diff --git a/app/src/lib/i18n/ar.ts b/app/src/lib/i18n/ar.ts index 0604658a5..a34a66002 100644 --- a/app/src/lib/i18n/ar.ts +++ b/app/src/lib/i18n/ar.ts @@ -845,6 +845,7 @@ const messages: TranslationMap = { 'onboarding.custom.memory.configureDesc': 'افحص الذاكرة أو صدّرها أو امسحها بنفسك. اضبطها من الإعدادات › الذاكرة.', 'accounts.addAccount': 'إضافة حساب', + 'accounts.addApps': 'إضافة تطبيقات', 'accounts.manageAccounts': 'إدارة الحسابات', 'accounts.noAccounts': 'لا توجد حسابات متصلة', 'accounts.connectAccount': 'اربط حسابًا للبدء', diff --git a/app/src/lib/i18n/bn.ts b/app/src/lib/i18n/bn.ts index c048c85ad..25c65fe95 100644 --- a/app/src/lib/i18n/bn.ts +++ b/app/src/lib/i18n/bn.ts @@ -867,6 +867,7 @@ const messages: TranslationMap = { 'onboarding.custom.memory.configureDesc': 'মেমোরি নিজে পরীক্ষা, এক্সপোর্ট বা মুছুন। Settings › Memory-এ কনফিগার করুন।', 'accounts.addAccount': 'অ্যাকাউন্ট যোগ করুন', + 'accounts.addApps': 'অ্যাপ যোগ করুন', 'accounts.manageAccounts': 'অ্যাকাউন্ট পরিচালনা', 'accounts.noAccounts': 'কোনো অ্যাকাউন্ট সংযুক্ত নেই', 'accounts.connectAccount': 'শুরু করতে একটি অ্যাকাউন্ট সংযুক্ত করুন', diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index ee95d1bee..a8d018ed2 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -889,6 +889,7 @@ const messages: TranslationMap = { 'onboarding.custom.memory.configureDesc': 'Überprüfe, exportiere oder lösche den Speicher selbst. Konfiguriere in den Einstellungen › Speicher.', 'accounts.addAccount': 'Konto hinzufügen', + 'accounts.addApps': 'Apps hinzufügen', 'accounts.manageAccounts': 'Konten verwalten', 'accounts.noAccounts': 'Keine Konten verbunden', 'accounts.connectAccount': 'Verbinde ein Konto, um loszulegen', diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index b3fc74342..89568271d 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -1192,6 +1192,7 @@ const en: TranslationMap = { // Accounts 'accounts.addAccount': 'Add Account', + 'accounts.addApps': 'Add apps', 'accounts.manageAccounts': 'Manage Accounts', 'accounts.noAccounts': 'No accounts connected', 'accounts.connectAccount': 'Connect an account to get started', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 46f3fdc16..882146007 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -884,6 +884,7 @@ const messages: TranslationMap = { 'onboarding.custom.memory.configureDesc': 'Inspecciona, exporta o borra la memoria tú mismo. Configura en Configuración › Memoria.', 'accounts.addAccount': 'Agregar cuenta', + 'accounts.addApps': 'Agregar apps', 'accounts.manageAccounts': 'Administrar cuentas', 'accounts.noAccounts': 'Sin cuentas conectadas', 'accounts.connectAccount': 'Conecta una cuenta para empezar', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index bffb86b2f..fdca1c8ab 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -887,6 +887,7 @@ const messages: TranslationMap = { 'onboarding.custom.memory.configureDesc': 'Inspecte, exporte ou efface la mémoire toi-même. À configurer dans Paramètres › Mémoire.', 'accounts.addAccount': 'Ajouter un compte', + 'accounts.addApps': 'Ajouter des apps', 'accounts.manageAccounts': 'Gérer les comptes', 'accounts.noAccounts': 'Aucun compte connecté', 'accounts.connectAccount': 'Connecte un compte pour démarrer', diff --git a/app/src/lib/i18n/hi.ts b/app/src/lib/i18n/hi.ts index 9bd80a35c..77bf2d9b3 100644 --- a/app/src/lib/i18n/hi.ts +++ b/app/src/lib/i18n/hi.ts @@ -863,6 +863,7 @@ const messages: TranslationMap = { 'onboarding.custom.memory.configureDesc': 'मेमोरी खुद देखें, एक्सपोर्ट करें या मिटाएं। Settings › Memory में कॉन्फिगर करें।', 'accounts.addAccount': 'अकाउंट जोड़ें', + 'accounts.addApps': 'ऐप्स जोड़ें', 'accounts.manageAccounts': 'अकाउंट मैनेज करें', 'accounts.noAccounts': 'कोई अकाउंट कनेक्ट नहीं है', 'accounts.connectAccount': 'शुरू करने के लिए एक अकाउंट कनेक्ट करें', diff --git a/app/src/lib/i18n/id.ts b/app/src/lib/i18n/id.ts index 28b7a6f3b..e6e29dc4f 100644 --- a/app/src/lib/i18n/id.ts +++ b/app/src/lib/i18n/id.ts @@ -870,6 +870,7 @@ const messages: TranslationMap = { 'onboarding.custom.memory.configureDesc': 'Periksa, ekspor, atau hapus memori sendiri. Konfigurasi di Pengaturan › Memori.', 'accounts.addAccount': 'Tambah Akun', + 'accounts.addApps': 'Tambah aplikasi', 'accounts.manageAccounts': 'Kelola Akun', 'accounts.noAccounts': 'Belum ada akun terhubung', 'accounts.connectAccount': 'Hubungkan akun untuk memulai', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 119947c8a..2d21172e5 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -881,6 +881,7 @@ const messages: TranslationMap = { 'onboarding.custom.memory.configureDesc': 'Ispeziona, esporta o cancella la memoria da solo. Configura in Impostazioni › Memoria.', 'accounts.addAccount': 'Aggiungi account', + 'accounts.addApps': 'Aggiungi app', 'accounts.manageAccounts': 'Gestisci account', 'accounts.noAccounts': 'Nessun account connesso', 'accounts.connectAccount': 'Connetti un account per iniziare', diff --git a/app/src/lib/i18n/ko.ts b/app/src/lib/i18n/ko.ts index 5bd9fb7be..45eaa36b6 100644 --- a/app/src/lib/i18n/ko.ts +++ b/app/src/lib/i18n/ko.ts @@ -864,6 +864,7 @@ const messages: TranslationMap = { 'onboarding.custom.memory.configureDesc': '메모리를 직접 검사, 내보내기 또는 삭제할 수 있습니다. 설정 › 메모리에서 구성할 수 있습니다.', 'accounts.addAccount': '계정 추가', + 'accounts.addApps': '앱 추가', 'accounts.manageAccounts': '계정 관리', 'accounts.noAccounts': '연결된 계정이 없습니다', 'accounts.connectAccount': '시작하려면 계정을 연결하세요', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 92b190eda..c945228fb 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -876,6 +876,7 @@ const messages: TranslationMap = { 'onboarding.custom.memory.configureDesc': 'Przeglądaj, eksportuj lub czyść pamięć samodzielnie. Skonfiguruj w Ustawieniach › Pamięć.', 'accounts.addAccount': 'Dodaj konto', + 'accounts.addApps': 'Dodaj aplikacje', 'accounts.manageAccounts': 'Zarządzaj kontami', 'accounts.noAccounts': 'Brak podłączonych kont', 'accounts.connectAccount': 'Podłącz konto, aby zacząć', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index 8e8fb1a79..681142d68 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -886,6 +886,7 @@ const messages: TranslationMap = { 'onboarding.custom.memory.configureDesc': 'Inspecione, exporte ou limpe a memória você mesmo. Configure em Configurações › Memória.', 'accounts.addAccount': 'Adicionar Conta', + 'accounts.addApps': 'Adicionar apps', 'accounts.manageAccounts': 'Gerenciar Contas', 'accounts.noAccounts': 'Nenhuma conta conectada', 'accounts.connectAccount': 'Conecte uma conta para começar', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index 8f55907d7..2dc065ba4 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -874,6 +874,7 @@ const messages: TranslationMap = { 'onboarding.custom.memory.configureDesc': 'Просматривай, экспортируй или очищай память самостоятельно. Настрой в Настройки › Память.', 'accounts.addAccount': 'Добавить аккаунт', + 'accounts.addApps': 'Добавить приложения', 'accounts.manageAccounts': 'Управление аккаунтами', 'accounts.noAccounts': 'Аккаунты не подключены', 'accounts.connectAccount': 'Подключи аккаунт для начала работы', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 9d7a8cdf2..6aa0b6d26 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -825,6 +825,7 @@ const messages: TranslationMap = { 'onboarding.custom.memory.defaultDesc': 'OpenHuman 会自动管理记忆存储和检索。无需设置。', 'onboarding.custom.memory.configureDesc': '你可以自行查看、导出或清除记忆。在设置 › 记忆中配置。', 'accounts.addAccount': '添加账户', + 'accounts.addApps': '添加应用', 'accounts.manageAccounts': '管理账户', 'accounts.noAccounts': '未连接任何账户', 'accounts.connectAccount': '连接一个账户以开始使用',