feat(sidebar): label the app-rail add button for new users (#4052)

This commit is contained in:
Cyrus Gray
2026-06-24 09:20:00 -07:00
committed by GitHub
parent 59317f5fb9
commit 8b4d4e7765
16 changed files with 73 additions and 16 deletions
@@ -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<string, typeof whatsappAccount> = { '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<string, number>,
},
};
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<typeof import('react-router-dom')>();
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) {
@@ -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')}>
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
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')}>
<svg className="h-4 w-4 flex-none" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
</svg>
{showAddLabel && (
<span className="whitespace-nowrap text-xs font-medium">{t('accounts.addApps')}</span>
)}
</button>
</div>
+1
View File
@@ -845,6 +845,7 @@ const messages: TranslationMap = {
'onboarding.custom.memory.configureDesc':
'افحص الذاكرة أو صدّرها أو امسحها بنفسك. اضبطها من الإعدادات › الذاكرة.',
'accounts.addAccount': 'إضافة حساب',
'accounts.addApps': 'إضافة تطبيقات',
'accounts.manageAccounts': 'إدارة الحسابات',
'accounts.noAccounts': 'لا توجد حسابات متصلة',
'accounts.connectAccount': 'اربط حسابًا للبدء',
+1
View File
@@ -867,6 +867,7 @@ const messages: TranslationMap = {
'onboarding.custom.memory.configureDesc':
'মেমোরি নিজে পরীক্ষা, এক্সপোর্ট বা মুছুন। Settings › Memory-এ কনফিগার করুন।',
'accounts.addAccount': 'অ্যাকাউন্ট যোগ করুন',
'accounts.addApps': 'অ্যাপ যোগ করুন',
'accounts.manageAccounts': 'অ্যাকাউন্ট পরিচালনা',
'accounts.noAccounts': 'কোনো অ্যাকাউন্ট সংযুক্ত নেই',
'accounts.connectAccount': 'শুরু করতে একটি অ্যাকাউন্ট সংযুক্ত করুন',
+1
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -863,6 +863,7 @@ const messages: TranslationMap = {
'onboarding.custom.memory.configureDesc':
'मेमोरी खुद देखें, एक्सपोर्ट करें या मिटाएं। Settings › Memory में कॉन्फिगर करें।',
'accounts.addAccount': 'अकाउंट जोड़ें',
'accounts.addApps': 'ऐप्स जोड़ें',
'accounts.manageAccounts': 'अकाउंट मैनेज करें',
'accounts.noAccounts': 'कोई अकाउंट कनेक्ट नहीं है',
'accounts.connectAccount': 'शुरू करने के लिए एक अकाउंट कनेक्ट करें',
+1
View File
@@ -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',
+1
View File
@@ -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',
+1
View File
@@ -864,6 +864,7 @@ const messages: TranslationMap = {
'onboarding.custom.memory.configureDesc':
'메모리를 직접 검사, 내보내기 또는 삭제할 수 있습니다. 설정 › 메모리에서 구성할 수 있습니다.',
'accounts.addAccount': '계정 추가',
'accounts.addApps': '앱 추가',
'accounts.manageAccounts': '계정 관리',
'accounts.noAccounts': '연결된 계정이 없습니다',
'accounts.connectAccount': '시작하려면 계정을 연결하세요',
+1
View File
@@ -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ąć',
+1
View File
@@ -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',
+1
View File
@@ -874,6 +874,7 @@ const messages: TranslationMap = {
'onboarding.custom.memory.configureDesc':
'Просматривай, экспортируй или очищай память самостоятельно. Настрой в Настройки › Память.',
'accounts.addAccount': 'Добавить аккаунт',
'accounts.addApps': 'Добавить приложения',
'accounts.manageAccounts': 'Управление аккаунтами',
'accounts.noAccounts': 'Аккаунты не подключены',
'accounts.connectAccount': 'Подключи аккаунт для начала работы',
+1
View File
@@ -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': '连接一个账户以开始使用',