From f28780abacd37df83ca3909d767e70e9f4fa9ab2 Mon Sep 17 00:00:00 2001 From: Arjun Mahanti Date: Tue, 25 Aug 2026 12:52:03 -0400 Subject: [PATCH] fix(desktop): polish live Bestie feed cards Co-authored-by: Arjun Mahanti Signed-off-by: Arjun Mahanti --- .../features/bestie/lib/bestieFeed.test.mjs | 30 ++++++ desktop/src/features/bestie/lib/bestieFeed.ts | 36 +++++++ .../src/features/bestie/ui/BestieFeedCard.tsx | 94 +++++++------------ desktop/tests/e2e/bestie-feed.spec.ts | 47 +++++++++- 4 files changed, 143 insertions(+), 64 deletions(-) diff --git a/desktop/src/features/bestie/lib/bestieFeed.test.mjs b/desktop/src/features/bestie/lib/bestieFeed.test.mjs index 760b99b67e..f31b9d90c9 100644 --- a/desktop/src/features/bestie/lib/bestieFeed.test.mjs +++ b/desktop/src/features/bestie/lib/bestieFeed.test.mjs @@ -3,6 +3,7 @@ import test from "node:test"; import { bestieItemSourceLabel, + bestieItemSummary, filterBestieFeedItems, getVisibleBestieFeedItems, reduceBestiePanelState, @@ -96,6 +97,35 @@ test("bestieItemSourceLabel uses only source metadata with a neutral fallback", ); }); +test("bestieItemSummary extracts a concise headline from actual Buzz text", () => { + const actualContent = + "Reminder: Review the launch plan before lunch. This second sentence contains implementation details that do not belong in the card headline."; + assert.equal( + bestieItemSummary( + item({ + groupItems: [{ ...item().item, content: actualContent }], + preview: actualContent, + subject: "Mention", + }), + ), + "Review the launch plan before lunch.", + ); +}); + +test("bestieItemSummary strips media and bounds long source text", () => { + const summary = bestieItemSummary( + item({ + preview: + "@arjun Here is the important source update with enough additional context to exceed the compact Bestie headline limit by a wide margin ![proof](https://example.com/proof.png)", + subject: "Mention", + }), + ); + assert.ok(summary.startsWith("Here is the important source update")); + assert.ok(summary.endsWith("…")); + assert.ok(Array.from(summary).length <= 96); + assert.doesNotMatch(summary, /proof|https/); +}); + test("reduceBestiePanelState keeps only one floating panel open", () => { const reply = reduceBestiePanelState( { mode: "closed" }, diff --git a/desktop/src/features/bestie/lib/bestieFeed.ts b/desktop/src/features/bestie/lib/bestieFeed.ts index b7c4ef0b19..279c49e584 100644 --- a/desktop/src/features/bestie/lib/bestieFeed.ts +++ b/desktop/src/features/bestie/lib/bestieFeed.ts @@ -1,5 +1,6 @@ import { isProjectInboxItem } from "@/features/home/lib/projectInbox"; import type { InboxItem } from "@/features/home/lib/inbox"; +import { summarizeMessageLinkContent } from "@/features/messages/lib/messageLinkMetadata"; export type BestieFeedFilter = "all" | "messages" | "tasks"; @@ -60,6 +61,41 @@ export function bestieItemSourceLabel(item: InboxItem) { return item.channelLabel ?? (item.item.channelName.trim() || "Buzz"); } +const BESTIE_SUMMARY_MAX_LENGTH = 96; + +function truncateBestieSummary(value: string) { + const characters = Array.from(value); + if (characters.length <= BESTIE_SUMMARY_MAX_LENGTH) return value; + + const clipped = characters.slice(0, BESTIE_SUMMARY_MAX_LENGTH - 1).join(""); + const lastSpace = clipped.lastIndexOf(" "); + const summary = lastSpace > 56 ? clipped.slice(0, lastSpace) : clipped; + return `${summary.trimEnd()}…`; +} + +/** Builds a concise extractive headline using only text from the Buzz item. */ +export function bestieItemSummary(item: InboxItem) { + if (isProjectInboxItem(item.item)) return item.subject; + + const latestContent = [...item.groupItems] + .sort((left, right) => right.createdAt - left.createdAt) + .find((event) => event.content.trim().length > 0)?.content; + const normalized = summarizeMessageLinkContent(latestContent ?? item.preview) + .replace(/^(?:agent progress|mention|reminder)\s*:\s*/i, "") + .replace(/^@[\p{L}\p{N}._-]+[:,]?\s*/u, "") + .trim(); + if (!normalized || normalized === "No message text") return item.subject; + + const sentenceEnd = normalized.search(/[.!?](?:\s|$)/); + const firstSentence = + sentenceEnd >= 0 ? normalized.slice(0, sentenceEnd + 1).trim() : normalized; + const [firstCharacter = "", ...remainingCharacters] = + Array.from(firstSentence); + return truncateBestieSummary( + `${firstCharacter.toLocaleUpperCase()}${remainingCharacters.join("")}`, + ); +} + export function reduceBestiePanelState( _state: BestiePanelState, action: BestiePanelAction, diff --git a/desktop/src/features/bestie/ui/BestieFeedCard.tsx b/desktop/src/features/bestie/ui/BestieFeedCard.tsx index 52c7ae6bed..7c0f05ece8 100644 --- a/desktop/src/features/bestie/ui/BestieFeedCard.tsx +++ b/desktop/src/features/bestie/ui/BestieFeedCard.tsx @@ -1,17 +1,13 @@ -import { - Bot, - Clock3, - GitPullRequest, - MessageSquareReply, - Sparkles, -} from "lucide-react"; +import { Clock3, MessageSquareReply, Sparkles } from "lucide-react"; -import { bestieItemSourceLabel } from "@/features/bestie/lib/bestieFeed"; +import { bestieItemSummary } from "@/features/bestie/lib/bestieFeed"; import type { InboxItem } from "@/features/home/lib/inbox"; -import { isProjectInboxItem } from "@/features/home/lib/projectInbox"; import { Badge } from "@/shared/ui/badge"; import { Button } from "@/shared/ui/button"; +import { parseImetaTags } from "@/shared/ui/markdown/parseImeta"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; +import { UserAvatar } from "@/shared/ui/UserAvatar"; +import { VideoReviewCommentMarkdown } from "@/shared/ui/VideoReviewCommentMarkdown"; type BestieFeedCardProps = { item: InboxItem; @@ -20,12 +16,12 @@ type BestieFeedCardProps = { onSnooze: () => void; }; -function actualSupportingContent(item: InboxItem) { +function actualSupportingItems(item: InboxItem) { const seen = new Set(); return [...item.groupItems] .sort((left, right) => left.createdAt - right.createdAt) - .map((event) => event.content.trim()) - .filter((content) => { + .filter((event) => { + const content = event.content.trim(); if (!content || seen.has(content)) return false; seen.add(content); return true; @@ -34,42 +30,31 @@ function actualSupportingContent(item: InboxItem) { } function SupportingVisual({ item }: { item: InboxItem }) { - const content = actualSupportingContent(item); - const projectItem = isProjectInboxItem(item.item); + const sourceItems = actualSupportingItems(item); return (
-
- {projectItem ? ( - - ) : item.categories.includes("agent_activity") ? ( - - ) : ( - - )} - - {bestieItemSourceLabel(item)} - - · - {item.categoryLabel} - {item.unreadCount > 0 ? · {item.unreadCount} unread : null} -
- {content.length > 0 ? ( -
- {content.map((message) => ( + {sourceItems.length > 0 ? ( +
+ {sourceItems.map((sourceItem) => (
- {message} +
))}
) : ( -

+

This Buzz event has no text content.

)} @@ -115,6 +100,7 @@ export function BestieFeedCard({ onSnooze, }: BestieFeedCardProps) { const headingId = `bestie-card-heading-${item.id}`; + const summary = bestieItemSummary(item); return (
- {item.avatarUrl ? ( - - ) : ( - - )} -
-

{item.senderLabel}

-

- {bestieItemSourceLabel(item)} -

-
+

- {item.subject} + {summary}

-

- {item.preview} -

@@ -186,14 +156,14 @@ export function BestieFeedCard({
diff --git a/desktop/tests/e2e/bestie-feed.spec.ts b/desktop/tests/e2e/bestie-feed.spec.ts index be7b6d6734..12be05442d 100644 --- a/desktop/tests/e2e/bestie-feed.spec.ts +++ b/desktop/tests/e2e/bestie-feed.spec.ts @@ -5,12 +5,34 @@ import { installMockBridge } from "../helpers/bridge"; const LIVE_MENTION = "Please review the release checklist."; const LIVE_REMINDER = "Reminder: update the launch plan before lunch."; +const MEDIA_SUMMARY = "The design reference is ready for review."; test("Bestie Feed projects only events returned by the Home feed", async ({ page, }) => { await installMockBridge(page); await page.goto("/"); + await page.waitForFunction( + () => typeof window.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__ === "function", + ); + await page.evaluate(() => { + window.__BUZZ_E2E_PUSH_MOCK_FEED_ITEM__?.({ + id: "bestie-media-evidence", + kind: 9, + pubkey: "a".repeat(64), + content: + "The design reference is ready for review. This additional implementation detail should remain supporting evidence rather than becoming the headline.\n\n![Reference image](/app-icon@2x.png)", + created_at: Math.floor(Date.now() / 1_000) + 1, + channel_id: "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50", + channel_name: "general", + channel_type: "stream", + tags: [ + ["h", "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50"], + ["p", "deadbeef".repeat(8)], + ], + category: "mention", + }); + }); await page.getByTestId("open-bestie-feed").click(); await expect(page).toHaveURL(/#\/\?view=bestie$/); @@ -24,6 +46,15 @@ test("Bestie Feed projects only events returned by the Home feed", async ({ await expect( page.getByText(LIVE_REMINDER, { exact: true }).first(), ).toBeVisible(); + const mediaCard = page + .getByRole("heading", { name: MEDIA_SUMMARY, exact: true }) + .locator("xpath=ancestor::article"); + await expect(mediaCard).toBeVisible(); + await expect( + mediaCard.getByRole("button", { + name: "Zoom image: Reference image", + }), + ).toBeVisible(); await expect(page.getByText(/Simon asked for the latest/)).toHaveCount(0); await expect(page.getByText(/Cmd\+K is ready/)).toHaveCount(0); @@ -38,8 +69,20 @@ test("Bestie Feed projects only events returned by the Home feed", async ({ .getByText(LIVE_MENTION, { exact: true }) .first() .locator("xpath=ancestor::article"); + await expect( + mentionCard.getByRole("heading", { name: LIVE_MENTION, exact: true }), + ).toBeVisible(); + await expect(mentionCard.getByText("Alice", { exact: true })).toHaveCount(0); + await expect(mentionCard.getByText("general", { exact: true })).toHaveCount( + 0, + ); + await expect( + mentionCard.locator( + '[data-testid^="bestie-avatar-"][data-testid$="-fallback"]', + ), + ).toBeVisible(); const snoozeTrigger = mentionCard.getByRole("button", { - name: /Snooze Mention/, + name: `Snooze ${LIVE_MENTION}`, }); await snoozeTrigger.click(); await expect(page.getByText(LIVE_MENTION, { exact: true })).toHaveCount(0); @@ -52,7 +95,7 @@ test("Bestie Feed projects only events returned by the Home feed", async ({ .getByText(LIVE_MENTION, { exact: true }) .first() .locator("xpath=ancestor::article") - .getByRole("button", { name: /Reply about Mention/ }); + .getByRole("button", { name: `Reply about ${LIVE_MENTION}` }); await replyTrigger.click(); const replyPanel = page.getByTestId("bestie-reply-panel"); await expect(replyPanel).toContainText(LIVE_MENTION);