From e506962ee325a433e986f98d8c0796e4914f2f2a Mon Sep 17 00:00:00 2001 From: Arjun Mahanti Date: Wed, 26 Aug 2026 17:55:20 -0400 Subject: [PATCH 1/3] feat(desktop): add Bestie sidebar entry Co-authored-by: Codex Signed-off-by: Arjun Mahanti --- desktop/package.json | 1 + desktop/playwright.bestie.config.ts | 25 +++++++++ .../src/features/sidebar/ui/AppSidebar.tsx | 2 + .../sidebar/ui/AppSidebarPinnedHeader.tsx | 50 +++++++++++++++++ desktop/tests/e2e/bestie-sidebar.spec.ts | 54 +++++++++++++++++++ 5 files changed, 132 insertions(+) create mode 100644 desktop/playwright.bestie.config.ts create mode 100644 desktop/tests/e2e/bestie-sidebar.spec.ts diff --git a/desktop/package.json b/desktop/package.json index e810d2bc284..3e9e176d88a 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -20,6 +20,7 @@ "test:e2e": "pnpm build:e2e && playwright test", "test:e2e:smoke": "pnpm build:e2e && playwright test --project=smoke", "test:e2e:integration": "pnpm build:e2e && playwright test --project=integration", + "test:e2e:bestie": "VITE_BUZZ_BESTIE=1 pnpm build:e2e && playwright test --config=playwright.bestie.config.ts", "test:e2e:release-smoke": "pnpm build:e2e && playwright test --config=playwright.release-smoke.config.ts", "test:e2e:report": "playwright show-report", "tauri:build": "tauri build" diff --git a/desktop/playwright.bestie.config.ts b/desktop/playwright.bestie.config.ts new file mode 100644 index 00000000000..e543e3e1cc1 --- /dev/null +++ b/desktop/playwright.bestie.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, devices } from "@playwright/test"; + +const webPort = process.env.BUZZ_BESTIE_E2E_WEB_PORT ?? "4174"; +const webUrl = `http://127.0.0.1:${webPort}`; + +export default defineConfig({ + testDir: "./tests/e2e", + testMatch: "**/bestie-sidebar.spec.ts", + timeout: 30_000, + retries: process.env.CI ? 2 : 0, + workers: 1, + reporter: [["list"]], + use: { + ...devices["Desktop Chrome"], + baseURL: webUrl, + screenshot: "only-on-failure", + trace: "on-first-retry", + }, + webServer: { + command: `python3 -m http.server ${webPort} -d dist`, + cwd: ".", + reuseExistingServer: false, + url: webUrl, + }, +}); diff --git a/desktop/src/features/sidebar/ui/AppSidebar.tsx b/desktop/src/features/sidebar/ui/AppSidebar.tsx index 50c9a80f6b1..19f4aa59dc5 100644 --- a/desktop/src/features/sidebar/ui/AppSidebar.tsx +++ b/desktop/src/features/sidebar/ui/AppSidebar.tsx @@ -552,7 +552,9 @@ export function AppSidebar({ data-testid="sidebar-scroll-content" > Promise; onSelectAgents: () => void; onSelectHome: () => void; onSelectProjects: () => void; @@ -89,7 +95,9 @@ export function AppSidebarPinnedHeader({ } export function AppSidebarPrimaryMenu({ + bestieRelayUrl, homeBadgeCount, + onOpenDm, onSelectAgents, onSelectHome, onSelectProjects, @@ -167,6 +175,12 @@ export function AppSidebarPrimaryMenu({ Agents + + + ); } + +function BestieSidebarMenuItem({ + onOpenDm, + relayUrl, +}: { + onOpenDm: (input: { pubkeys: string[] }) => Promise; + relayUrl?: string | null; +}) { + const managedAgentsQuery = useManagedAgentsQuery(); + const bestieAgent = React.useMemo( + () => pickBestieAgent(managedAgentsQuery.data ?? [], relayUrl), + [managedAgentsQuery.data, relayUrl], + ); + + if (!bestieAgent) return null; + + return ( + + void onOpenDm({ pubkeys: [bestieAgent.pubkey] })} + tooltip={`Message ${bestieAgent.name}`} + type="button" + > + + Bestie + + + ); +} diff --git a/desktop/tests/e2e/bestie-sidebar.spec.ts b/desktop/tests/e2e/bestie-sidebar.spec.ts new file mode 100644 index 00000000000..dc15ebb599c --- /dev/null +++ b/desktop/tests/e2e/bestie-sidebar.spec.ts @@ -0,0 +1,54 @@ +import { expect, test } from "@playwright/test"; + +import { installMockBridge } from "../helpers/bridge"; +import { FEATURE_OVERRIDES_STORAGE_KEY } from "../helpers/features"; + +const BESTIE_PUBKEY = + "be571e0000000000000000000000000000000000000000000000000000000000"; + +const bestie = { + avatarUrl: null, + name: "Bestie", + personaId: "builtin:bestie", + pubkey: BESTIE_PUBKEY, + status: "running" as const, +}; + +test("the enabled Bestie experiment adds a direct-message entry below Agents", async ({ + page, +}) => { + await installMockBridge(page, { managedAgents: [bestie] }); + await page.goto("/"); + + const agentsEntry = page.getByTestId("open-agents-view"); + const bestieEntry = page.getByTestId("open-bestie-dm"); + await expect(bestieEntry).toBeVisible(); + await expect(bestieEntry).toContainText("Bestie"); + + const [agentsBox, bestieBox] = await Promise.all([ + agentsEntry.boundingBox(), + bestieEntry.boundingBox(), + ]); + expect(agentsBox).not.toBeNull(); + expect(bestieBox).not.toBeNull(); + expect(bestieBox?.y).toBeGreaterThan(agentsBox?.y ?? 0); + + await bestieEntry.click(); + await expect(page.getByTestId("chat-title")).toHaveText("Bestie"); +}); + +test("the disabled Bestie experiment does not mount the sidebar entry", async ({ + page, +}) => { + await installMockBridge(page, { managedAgents: [bestie] }); + await page.addInitScript((key) => { + const overrides = JSON.parse( + window.localStorage.getItem(key) ?? "{}", + ) as Record; + overrides.bestie = false; + window.localStorage.setItem(key, JSON.stringify(overrides)); + }, FEATURE_OVERRIDES_STORAGE_KEY); + await page.goto("/"); + + await expect(page.getByTestId("open-bestie-dm")).toHaveCount(0); +}); From e84453d7ade0b884a3c383b992ad575736e52a82 Mon Sep 17 00:00:00 2001 From: Arjun Mahanti Date: Wed, 26 Aug 2026 20:09:35 -0400 Subject: [PATCH 2/3] fix(desktop): scope Bestie sidebar navigation Co-authored-by: Codex Signed-off-by: Arjun Mahanti --- .github/workflows/ci.yml | 3 + desktop/src/app/AppShell.tsx | 16 ++-- desktop/src/app/useScopedOpenDmNavigation.ts | 45 +++++++++++ .../src/features/sidebar/ui/AppSidebar.tsx | 1 + .../features/sidebar/ui/AppSidebar.types.ts | 3 +- .../sidebar/ui/AppSidebarPinnedHeader.tsx | 23 +++++- desktop/tests/e2e/bestie-sidebar.spec.ts | 79 +++++++++++++++++++ 7 files changed, 156 insertions(+), 14 deletions(-) create mode 100644 desktop/src/app/useScopedOpenDmNavigation.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8308c9449bc..e2c590a9708 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -288,6 +288,9 @@ jobs: if: ${{ !cancelled() }} run: node scripts/summarize-flaky-tests.mjs playwright-report.json "Desktop Smoke E2E (${{ matrix.shard }})" working-directory: desktop + - name: Bestie experiment e2e + if: matrix.shard == 1 + run: pnpm -C desktop test:e2e:bestie - name: Upload desktop smoke e2e artifacts if: ${{ !cancelled() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index eb5ab5a95d8..0ce76b0cf53 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -22,6 +22,7 @@ import { useSettingsShortcuts } from "@/app/useSettingsShortcuts"; import { useAppShellKeyboardShortcuts } from "@/app/useAppShellKeyboardShortcuts"; import { useAppShellDesktopNotifications } from "@/app/useAppShellDesktopNotifications"; import { useAppShellLifecycleEffects } from "@/app/useAppShellLifecycleEffects"; +import { useScopedOpenDmNavigation } from "@/app/useScopedOpenDmNavigation"; import { useChannelActivityProjection } from "@/app/useChannelActivityProjection"; import { useTauriWindowDrag } from "@/app/useTauriWindowDrag"; import { useWebviewZoomShortcuts } from "@/app/useWebviewZoomShortcuts"; @@ -32,7 +33,6 @@ import { useChannelsQuery, useCreateChannelMutation, useHideDmMutation, - useOpenDmMutation, } from "@/features/channels/hooks"; import { useDmResurfaceFromMessages } from "@/features/channels/useDmResurfaceFromMessages"; import { useUnreadChannels } from "@/features/channels/useUnreadChannels"; @@ -504,7 +504,11 @@ export function AppShell() { const createChannelMutation = useCreateChannelMutation(), createForumMutation = useCreateChannelMutation(); const { applyCanvas, applyAgents } = useApplyTemplate(); - const openDmMutation = useOpenDmMutation(); + const openDm = useScopedOpenDmNavigation({ + goChannel, + relayUrl: communitiesHook.activeCommunity?.relayUrl, + signerPubkey: identityQuery.data?.pubkey, + }); const hideDmMutation = useHideDmMutation(); useDmResurfaceFromMessages({ pubkey: identityQuery.data?.pubkey, @@ -870,13 +874,7 @@ export function AppShell() { onMarkChannelRead={markChannelRead} onMarkChannelUnread={markChannelUnread} onBrowseChannels={handleOpenBrowseChannels} - onOpenDm={async ({ pubkeys }) => { - const directMessage = - await openDmMutation.mutateAsync({ - pubkeys, - }); - await goChannel(directMessage.id); - }} + onOpenDm={openDm} onSelectAgents={() => void goAgents()} onSelectChannel={handleSidebarChannelSelect} onOpenSearchResult={handleOpenSearchResult} diff --git a/desktop/src/app/useScopedOpenDmNavigation.ts b/desktop/src/app/useScopedOpenDmNavigation.ts new file mode 100644 index 00000000000..8cb00398a48 --- /dev/null +++ b/desktop/src/app/useScopedOpenDmNavigation.ts @@ -0,0 +1,45 @@ +import * as React from "react"; + +import { canonicalRelayUrl } from "@/features/agents/managedAgentRuntimeStatus"; +import { useOpenDmMutation } from "@/features/channels/hooks"; +import type { OpenDmInput } from "@/shared/api/tauriChannels"; + +type OpenDmScope = { + relayUrl?: string; + signerPubkey?: string; +}; + +export function useScopedOpenDmNavigation({ + goChannel, + relayUrl, + signerPubkey, +}: OpenDmScope & { + goChannel: (channelId: string) => Promise; +}) { + const openDmMutation = useOpenDmMutation(); + const scopeRef = React.useRef({}); + scopeRef.current = { relayUrl, signerPubkey }; + + return React.useCallback( + async (input: OpenDmInput) => { + const directMessage = await openDmMutation.mutateAsync(input); + const currentScope = scopeRef.current; + if ( + input.expectedRelayUrl && + canonicalRelayUrl(input.expectedRelayUrl) !== + canonicalRelayUrl(currentScope.relayUrl ?? "") + ) { + return; + } + if ( + input.expectedSignerPubkey && + input.expectedSignerPubkey.toLowerCase() !== + currentScope.signerPubkey?.toLowerCase() + ) { + return; + } + await goChannel(directMessage.id); + }, + [goChannel, openDmMutation], + ); +} diff --git a/desktop/src/features/sidebar/ui/AppSidebar.tsx b/desktop/src/features/sidebar/ui/AppSidebar.tsx index 19f4aa59dc5..d0ffad0096a 100644 --- a/desktop/src/features/sidebar/ui/AppSidebar.tsx +++ b/desktop/src/features/sidebar/ui/AppSidebar.tsx @@ -553,6 +553,7 @@ export function AppSidebar({ > void; onMarkAllChannelsRead: () => void; onBrowseChannels?: (onCreated?: (channelId: string) => void) => void; - onOpenDm: (input: { pubkeys: string[] }) => Promise; + onOpenDm: (input: OpenDmInput) => Promise; onUpdateCommunity: ( id: string, updates: Partial>, diff --git a/desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx b/desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx index 77632262537..8f108dbdd80 100644 --- a/desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx +++ b/desktop/src/features/sidebar/ui/AppSidebarPinnedHeader.tsx @@ -7,6 +7,7 @@ import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar"; import { TopbarSearch } from "@/features/search/ui/TopbarSearch"; import { SidebarProjectsSection } from "@/features/sidebar/ui/SidebarProjectsSection"; import { FeatureGate } from "@/shared/features"; +import type { OpenDmInput } from "@/shared/api/tauriChannels"; import type { Channel, SearchHit } from "@/shared/api/types"; import { SidebarHeader, @@ -33,7 +34,7 @@ type AppSidebarPinnedHeaderProps = { onBrowseChannels?: () => void; onCreateAgent: () => void; onCreateChannel: () => void; - onOpenDm: (input: { pubkeys: string[] }) => Promise; + onOpenDm: (input: OpenDmInput) => Promise; onOpenSearchResult: (hit: SearchHit, query: string) => void; onSelectChannel: (channelId: string) => void; searchChannels: Channel[]; @@ -44,8 +45,9 @@ type AppSidebarPinnedHeaderProps = { type AppSidebarPrimaryMenuProps = { bestieRelayUrl?: string | null; + currentPubkey?: string; homeBadgeCount: number; - onOpenDm: (input: { pubkeys: string[] }) => Promise; + onOpenDm: (input: OpenDmInput) => Promise; onSelectAgents: () => void; onSelectHome: () => void; onSelectProjects: () => void; @@ -96,6 +98,7 @@ export function AppSidebarPinnedHeader({ export function AppSidebarPrimaryMenu({ bestieRelayUrl, + currentPubkey, homeBadgeCount, onOpenDm, onSelectAgents, @@ -177,6 +180,7 @@ export function AppSidebarPrimaryMenu({ @@ -203,10 +207,12 @@ export function AppSidebarPrimaryMenu({ } function BestieSidebarMenuItem({ + currentPubkey, onOpenDm, relayUrl, }: { - onOpenDm: (input: { pubkeys: string[] }) => Promise; + currentPubkey?: string; + onOpenDm: (input: OpenDmInput) => Promise; relayUrl?: string | null; }) { const managedAgentsQuery = useManagedAgentsQuery(); @@ -221,7 +227,16 @@ function BestieSidebarMenuItem({ void onOpenDm({ pubkeys: [bestieAgent.pubkey] })} + onClick={() => { + const expectedRelayUrl = relayUrl?.trim(); + const expectedSignerPubkey = currentPubkey?.trim(); + if (!expectedRelayUrl || !expectedSignerPubkey) return; + void onOpenDm({ + expectedRelayUrl, + expectedSignerPubkey, + pubkeys: [bestieAgent.pubkey], + }); + }} tooltip={`Message ${bestieAgent.name}`} type="button" > diff --git a/desktop/tests/e2e/bestie-sidebar.spec.ts b/desktop/tests/e2e/bestie-sidebar.spec.ts index dc15ebb599c..4a449954248 100644 --- a/desktop/tests/e2e/bestie-sidebar.spec.ts +++ b/desktop/tests/e2e/bestie-sidebar.spec.ts @@ -5,15 +5,46 @@ import { FEATURE_OVERRIDES_STORAGE_KEY } from "../helpers/features"; const BESTIE_PUBKEY = "be571e0000000000000000000000000000000000000000000000000000000000"; +const OWNER_PUBKEY = "deadbeef".repeat(8); +const RELAY_A = "ws://localhost:3000"; +const COMMUNITY_A = { + addedAt: "2026-01-01T00:00:00.000Z", + id: "bestie-community-a", + name: "Alpha", + relayUrl: RELAY_A, +}; +const COMMUNITY_B = { + addedAt: "2026-01-02T00:00:00.000Z", + id: "bestie-community-b", + name: "Bravo", + relayUrl: "ws://localhost:3001", +}; const bestie = { avatarUrl: null, name: "Bestie", personaId: "builtin:bestie", pubkey: BESTIE_PUBKEY, + relayUrl: RELAY_A, status: "running" as const, }; +async function seedCommunities( + page: import("@playwright/test").Page, + activeId = COMMUNITY_A.id, +) { + await page.addInitScript( + ({ active, communities }) => { + window.localStorage.setItem( + "buzz-communities", + JSON.stringify(communities), + ); + window.localStorage.setItem("buzz-active-community-id", active); + }, + { active: activeId, communities: [COMMUNITY_A, COMMUNITY_B] }, + ); +} + test("the enabled Bestie experiment adds a direct-message entry below Agents", async ({ page, }) => { @@ -52,3 +83,51 @@ test("the disabled Bestie experiment does not mount the sidebar entry", async ({ await expect(page.getByTestId("open-bestie-dm")).toHaveCount(0); }); + +test("a delayed Bestie open is scoped to its rendered community and signer", async ({ + page, +}) => { + await installMockBridge( + page, + { managedAgents: [bestie], openDmDelayMs: 1_000 }, + { skipCommunitySeed: true }, + ); + await seedCommunities(page); + await page.goto("/"); + + await page.getByTestId("open-bestie-dm").click(); + await expect + .poll(() => + page.evaluate( + () => + window.__BUZZ_E2E_COMMAND_LOG__?.findLast( + (entry) => entry.command === "open_dm", + )?.payload, + ), + ) + .toBeTruthy(); + const openDmPayload = await page.evaluate( + () => + window.__BUZZ_E2E_COMMAND_LOG__?.findLast( + (entry) => entry.command === "open_dm", + )?.payload, + ); + expect(openDmPayload).toMatchObject({ + expectedRelayUrl: RELAY_A, + expectedSignerPubkey: OWNER_PUBKEY, + pubkeys: [BESTIE_PUBKEY], + }); + + await page.getByTestId(`community-rail-button-${COMMUNITY_B.id}`).click(); + await expect + .poll(() => + page.evaluate(() => + window.localStorage.getItem("buzz-active-community-id"), + ), + ) + .toBe(COMMUNITY_B.id); + + await page.waitForTimeout(1_150); + await expect(page.getByTestId("chat-title")).toHaveCount(0); + await expect(page.getByTestId("open-bestie-dm")).toHaveCount(0); +}); From 9bea9aacccce509638173775b6eb36199875660b Mon Sep 17 00:00:00 2001 From: Fizz Date: Fri, 28 Aug 2026 11:26:29 -0400 Subject: [PATCH 3/3] fix(desktop): retain hidden DM reopen mutation On-behalf-of: mahanti Signed-off-by: Fizz Co-authored-by: Codex --- desktop/src/app/AppShell.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/desktop/src/app/AppShell.tsx b/desktop/src/app/AppShell.tsx index 0ce76b0cf53..f72e9578e77 100644 --- a/desktop/src/app/AppShell.tsx +++ b/desktop/src/app/AppShell.tsx @@ -33,6 +33,7 @@ import { useChannelsQuery, useCreateChannelMutation, useHideDmMutation, + useOpenDmMutation, } from "@/features/channels/hooks"; import { useDmResurfaceFromMessages } from "@/features/channels/useDmResurfaceFromMessages"; import { useUnreadChannels } from "@/features/channels/useUnreadChannels"; @@ -504,6 +505,7 @@ export function AppShell() { const createChannelMutation = useCreateChannelMutation(), createForumMutation = useCreateChannelMutation(); const { applyCanvas, applyAgents } = useApplyTemplate(); + const openDmMutation = useOpenDmMutation(); const openDm = useScopedOpenDmNavigation({ goChannel, relayUrl: communitiesHook.activeCommunity?.relayUrl,