diff --git a/app/src/components/layout/shell/SidebarAppRail.test.tsx b/app/src/components/layout/shell/SidebarAppRail.test.tsx index 8510c72bb..024807e2b 100644 --- a/app/src/components/layout/shell/SidebarAppRail.test.tsx +++ b/app/src/components/layout/shell/SidebarAppRail.test.tsx @@ -2,6 +2,7 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { purgeWebviewAccount } from '../../../services/webviewAccountService'; import SidebarAppRail from './SidebarAppRail'; const mockNavigate = vi.fn(); @@ -96,6 +97,57 @@ describe('SidebarAppRail', () => { expect(addButton).not.toHaveTextContent('accounts.addApps'); expect(addButton).toHaveAttribute('aria-label', 'accounts.addApps'); }); + + it('drops the account from state synchronously on disconnect, before the async purge (#4695)', () => { + setAccounts(['acct-whatsapp']); + // Hold the purge pending so we can prove `removeAccount` was dispatched + // *before* the purge (which triggers the app re-mount) resolves. The old + // order (await purge → removeAccount) let the re-mount re-open the + // just-purged webview because the account was still in the store. + let resolvePurge: () => void = () => {}; + vi.mocked(purgeWebviewAccount).mockReturnValue( + new Promise(res => { + resolvePurge = res; + }) + ); + + renderRail('/chat'); + + fireEvent.contextMenu(screen.getByRole('button', { name: 'WhatsApp' })); + fireEvent.click(screen.getByText('accounts.disconnect')); + + // removeAccount is dispatched while the purge is still pending. + expect(mockDispatch).toHaveBeenCalledWith({ + type: 'accounts/removeAccount', + payload: { accountId: 'acct-whatsapp' }, + }); + expect(purgeWebviewAccount).toHaveBeenCalledWith('acct-whatsapp'); + + resolvePurge(); + }); + + it('still drops the account and swallows the error when the purge rejects (#4695)', async () => { + setAccounts(['acct-whatsapp']); + // A purge failure must not leave the user with a zombie icon: the account is + // already removed from state before the await, and the rejection is caught + // and logged (not surfaced) so the disconnect handler resolves cleanly. + vi.mocked(purgeWebviewAccount).mockRejectedValue(new Error('purge failed')); + + renderRail('/chat'); + + fireEvent.contextMenu(screen.getByRole('button', { name: 'WhatsApp' })); + fireEvent.click(screen.getByText('accounts.disconnect')); + + expect(mockDispatch).toHaveBeenCalledWith({ + type: 'accounts/removeAccount', + payload: { accountId: 'acct-whatsapp' }, + }); + expect(purgeWebviewAccount).toHaveBeenCalledWith('acct-whatsapp'); + + // Flush microtasks so the awaited purge rejection reaches the handler's + // catch (which logs and swallows it) — the disconnect must not throw. + await new Promise(resolve => setTimeout(resolve, 0)); + }); }); function renderRail(route: string) { diff --git a/app/src/components/layout/shell/SidebarAppRail.tsx b/app/src/components/layout/shell/SidebarAppRail.tsx index 3024308b9..8892ba177 100644 --- a/app/src/components/layout/shell/SidebarAppRail.tsx +++ b/app/src/components/layout/shell/SidebarAppRail.tsx @@ -196,14 +196,23 @@ export default function SidebarAppRail() { account_status: account.status ?? 'unknown', }); } + // Drop the account from state FIRST, synchronously, before the async purge + // (#4695). `purgeWebviewAccount` tears down the provider's backend scanners, + // which triggers a full app-state re-fetch and UI re-mount. If the account + // were still in the store at that moment, the re-mount would remount + // `WebviewHost` for it and re-open (reveal) the just-purged provider webview + // — a fresh, logged-out CEF child-view pinned over the content area, + // swallowing all input. Removing it first unmounts the host (whose cleanup + // hides the child-view) and guarantees no re-open ever targets a + // disconnected account. + dispatch(removeAccount({ accountId })); try { await purgeWebviewAccount(accountId); } catch { - // Purge failures are already logged by the service; still drop the - // account from the UI so the user isn't stuck with a zombie icon. - debug('purge failed for %s — dropping from UI anyway', accountId); + // Purge failures are already logged by the service; the account is + // already gone from the UI so the user isn't stuck with a zombie icon. + debug('purge failed for %s — already dropped from UI', accountId); } - dispatch(removeAccount({ accountId })); }; // Close the context menu on Escape or any outside click. diff --git a/app/src/lib/i18n/de.ts b/app/src/lib/i18n/de.ts index 7034bb26e..2f4634f0e 100644 --- a/app/src/lib/i18n/de.ts +++ b/app/src/lib/i18n/de.ts @@ -205,7 +205,8 @@ const messages: TranslationMap = { 'orchPage.discover.notDiscoverableGuide': 'Registriere ein @handle, damit andere Agenten dich finden und dir schreiben können.', 'orchPage.discover.linkTitle': 'Neuen Agenten verknüpfen', - 'orchPage.discover.linkDescription': 'Füge eine Agenten-ID ein, um eine Verbindungsanfrage zu senden.', + 'orchPage.discover.linkDescription': + 'Füge eine Agenten-ID ein, um eine Verbindungsanfrage zu senden.', 'orchPage.discover.noRequests': 'Keine eingehenden Anfragen.', 'orchPage.usage.nav': 'Nutzung', 'orchPage.usage.connections': 'Verbindungen', diff --git a/app/src/lib/i18n/es.ts b/app/src/lib/i18n/es.ts index 67c3dab46..7874703e1 100644 --- a/app/src/lib/i18n/es.ts +++ b/app/src/lib/i18n/es.ts @@ -202,7 +202,8 @@ const messages: TranslationMap = { 'orchPage.discover.notDiscoverableGuide': 'Registra un @handle para que otros agentes puedan encontrarte y escribirte.', 'orchPage.discover.linkTitle': 'Vincular un nuevo agente', - 'orchPage.discover.linkDescription': 'Pega un ID de agente para enviar una solicitud de conexión.', + 'orchPage.discover.linkDescription': + 'Pega un ID de agente para enviar una solicitud de conexión.', 'orchPage.discover.noRequests': 'No hay solicitudes entrantes.', 'orchPage.usage.nav': 'Uso', 'orchPage.usage.connections': 'Conexiones', diff --git a/app/src/lib/i18n/fr.ts b/app/src/lib/i18n/fr.ts index 5b2cc5dd8..a5ca6bff5 100644 --- a/app/src/lib/i18n/fr.ts +++ b/app/src/lib/i18n/fr.ts @@ -190,7 +190,8 @@ const messages: TranslationMap = { 'orchPage.agent.description': "Discutez avec l'agent principal et observez son subconscient", 'orchPage.connections.nav': 'Connexions', 'orchPage.connections.title': 'Agents liés', - 'orchPage.connections.description': 'Pairs avec lesquels votre agent principal peut se coordonner', + 'orchPage.connections.description': + 'Pairs avec lesquels votre agent principal peut se coordonner', 'orchPage.connections.empty': 'Aucune connexion pour le moment.', 'orchPage.connections.emptyCta': 'Ajouter une connexion', 'orchPage.connections.statContacts': 'Connexions', @@ -202,7 +203,8 @@ const messages: TranslationMap = { 'orchPage.discover.notDiscoverableGuide': "Enregistrez un @handle pour que d'autres agents puissent vous trouver et vous écrire.", 'orchPage.discover.linkTitle': 'Lier un nouvel agent', - 'orchPage.discover.linkDescription': "Collez un ID d'agent pour envoyer une demande de connexion.", + 'orchPage.discover.linkDescription': + "Collez un ID d'agent pour envoyer une demande de connexion.", 'orchPage.discover.noRequests': 'Aucune demande entrante.', 'orchPage.usage.nav': 'Utilisation', 'orchPage.usage.connections': 'Connexions', diff --git a/app/src/lib/i18n/it.ts b/app/src/lib/i18n/it.ts index 09f0e1601..136cfd5a0 100644 --- a/app/src/lib/i18n/it.ts +++ b/app/src/lib/i18n/it.ts @@ -202,7 +202,8 @@ const messages: TranslationMap = { 'orchPage.discover.notDiscoverableGuide': 'Registra un @handle così altri agenti possono trovarti e scriverti.', 'orchPage.discover.linkTitle': 'Collega un nuovo agente', - 'orchPage.discover.linkDescription': 'Incolla un ID agente per inviare una richiesta di connessione.', + 'orchPage.discover.linkDescription': + 'Incolla un ID agente per inviare una richiesta di connessione.', 'orchPage.discover.noRequests': 'Nessuna richiesta in arrivo.', 'orchPage.usage.nav': 'Utilizzo', 'orchPage.usage.connections': 'Connessioni', diff --git a/app/src/lib/i18n/pl.ts b/app/src/lib/i18n/pl.ts index 3bccc102c..1e50f418a 100644 --- a/app/src/lib/i18n/pl.ts +++ b/app/src/lib/i18n/pl.ts @@ -204,7 +204,8 @@ const messages: TranslationMap = { 'orchPage.discover.notDiscoverableGuide': 'Zarejestruj @handle, aby inni agenci mogli Cię znaleźć i napisać do Ciebie.', 'orchPage.discover.linkTitle': 'Połącz nowego agenta', - 'orchPage.discover.linkDescription': 'Wklej identyfikator agenta, aby wysłać prośbę o połączenie.', + 'orchPage.discover.linkDescription': + 'Wklej identyfikator agenta, aby wysłać prośbę o połączenie.', 'orchPage.discover.noRequests': 'Brak przychodzących próśb.', 'orchPage.usage.nav': 'Wykorzystanie', 'orchPage.usage.connections': 'Połączenia', diff --git a/app/src/lib/i18n/pt.ts b/app/src/lib/i18n/pt.ts index a856d2e12..9b1e99556 100644 --- a/app/src/lib/i18n/pt.ts +++ b/app/src/lib/i18n/pt.ts @@ -201,7 +201,8 @@ const messages: TranslationMap = { 'orchPage.discover.notDiscoverableGuide': 'Registre um @handle para que outros agentes possam encontrá-lo e enviar mensagens.', 'orchPage.discover.linkTitle': 'Vincular um novo agente', - 'orchPage.discover.linkDescription': 'Cole um ID de agente para enviar uma solicitação de conexão.', + 'orchPage.discover.linkDescription': + 'Cole um ID de agente para enviar uma solicitação de conexão.', 'orchPage.discover.noRequests': 'Nenhuma solicitação recebida.', 'orchPage.usage.nav': 'Uso', 'orchPage.usage.connections': 'Conexões', diff --git a/app/src/lib/i18n/ru.ts b/app/src/lib/i18n/ru.ts index 931b53471..b79720bd6 100644 --- a/app/src/lib/i18n/ru.ts +++ b/app/src/lib/i18n/ru.ts @@ -192,7 +192,8 @@ const messages: TranslationMap = { 'orchPage.agent.description': 'Общайтесь с главным агентом и наблюдайте за его подсознанием', 'orchPage.connections.nav': 'Связи', 'orchPage.connections.title': 'Связанные агенты', - 'orchPage.connections.description': 'Партнёры, с которыми может координироваться ваш главный агент', + 'orchPage.connections.description': + 'Партнёры, с которыми может координироваться ваш главный агент', 'orchPage.connections.empty': 'Пока нет связей.', 'orchPage.connections.emptyCta': 'Добавить связь', 'orchPage.connections.statContacts': 'Связи',