From 1f1043a4ad92d314ff356a64527fff1cc1536cf7 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Tue, 23 Jun 2026 14:45:05 -0400 Subject: [PATCH] fix(desktop): prevent process text truncation Co-authored-by: Thomas Petersen Signed-off-by: Thomas Petersen --- desktop/playwright.config.ts | 1 + .../features/channels/ui/BotActivityBar.tsx | 18 +- .../src/features/channels/ui/ChannelPane.tsx | 2 +- .../messages/ui/MessageThreadPanel.tsx | 12 +- .../e2e/process-text-screenshots.spec.ts | 172 ++++++++++++++++++ 5 files changed, 193 insertions(+), 12 deletions(-) create mode 100644 desktop/tests/e2e/process-text-screenshots.spec.ts diff --git a/desktop/playwright.config.ts b/desktop/playwright.config.ts index 7f7ccce4f..b8221307a 100644 --- a/desktop/playwright.config.ts +++ b/desktop/playwright.config.ts @@ -34,6 +34,7 @@ export default defineConfig({ "**/team-management-screenshots.spec.ts", "**/active-turn-screenshots.spec.ts", "**/active-turn-resilience-screenshots.spec.ts", + "**/process-text-screenshots.spec.ts", "**/profile-active-turn-screenshots.spec.ts", "**/file-attachment.spec.ts", "**/video-attachment.spec.ts", diff --git a/desktop/src/features/channels/ui/BotActivityBar.tsx b/desktop/src/features/channels/ui/BotActivityBar.tsx index d9139935f..cd37dfcf9 100644 --- a/desktop/src/features/channels/ui/BotActivityBar.tsx +++ b/desktop/src/features/channels/ui/BotActivityBar.tsx @@ -163,7 +163,7 @@ export function BotActivityComposerAction({ className={cn( "inline-flex items-center justify-center rounded-full border border-border/60 bg-background font-medium text-muted-foreground transition-colors hover:border-primary/30 hover:bg-primary/5 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring data-[state=open]:border-primary/40 data-[state=open]:bg-primary/10 data-[state=open]:text-primary", isInline - ? "h-7 min-w-0 gap-2 overflow-visible border-transparent bg-transparent px-0 text-xs font-semibold leading-none shadow-none hover:border-transparent hover:bg-transparent data-[state=open]:border-transparent data-[state=open]:bg-transparent" + ? "h-7 min-w-0 max-w-full gap-2 overflow-hidden border-transparent bg-transparent px-0 text-left text-xs font-semibold leading-none shadow-none hover:border-transparent hover:bg-transparent data-[state=open]:border-transparent data-[state=open]:bg-transparent" : "h-9 min-w-9 gap-1.5 px-2 text-xs", )} data-testid="bot-activity-composer-trigger" @@ -177,7 +177,7 @@ export function BotActivityComposerAction({ onMouseLeave={closeWithDelay} type="button" > - + {typingAgents.slice(0, 2).map((agent) => ( {typingAgents.length > 2 ? ( - + +{typingAgents.length - 2} ) : null} - - {isInline ? {visibleStatusLabel} : "working"} + + {isInline ? ( + + {visibleStatusLabel} + + ) : ( + "working" + )} {isInline ? null : ( diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index 79c5e7939..d3ff0f4c0 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -765,7 +765,7 @@ export const ChannelPane = React.memo(function ChannelPane({
{hasComposerBotActivity ? ( -
+
void; onUnfollowThread?: () => void; }; - -/** Stable `useDeferredValue` initial value; mirrors `EMPTY_MESSAGES`. */ -const EMPTY_THREAD_REPLIES: MainTimelineEntry[] = []; -const THREAD_PANEL_MESSAGE_GUTTER_CLASS = "px-2"; -const THREAD_PANEL_COMPOSER_GUTTER_CLASS = "px-5"; +const EMPTY_THREAD_REPLIES: MainTimelineEntry[] = [], + THREAD_PANEL_MESSAGE_GUTTER_CLASS = "px-2", + THREAD_PANEL_COMPOSER_GUTTER_CLASS = "px-5"; const THREAD_PANEL_SUMMARY_INDENT_OFFSET_PX = -2; type MessageThreadPanelSkeletonProps = { @@ -908,7 +906,9 @@ export function MessageThreadPanel({ >
{toolbarExtraActions ? ( -
{toolbarExtraActions}
+
+ {toolbarExtraActions} +
) : null} {threadTypingPubkeys.length > 0 ? ( { + return page.evaluate( + ({ ch, k }) => + ( + window as Window & { + __BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?: (input: { + channelName: string; + kind?: number; + }) => boolean; + } + ).__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({ + channelName: ch, + kind: k, + }) ?? false, + { ch: channelName, k: kind }, + ); + }) + .toBe(true); +} + +async function seedMainBotTyping(page: Page) { + await waitForMockLiveSubscription(page, "general", KIND_TYPING_INDICATOR); + await page.evaluate((pubkey) => { + ( + window as Window & { + __BUZZ_E2E_EMIT_MOCK_TYPING__?: (input: { + channelName: string; + pubkey?: string; + }) => unknown; + } + ).__BUZZ_E2E_EMIT_MOCK_TYPING__?.({ channelName: "general", pubkey }); + }, AGENT_PUBKEY); +} + +async function seedThreadBotTyping(page: Page) { + await page.evaluate((pubkey) => { + ( + window as Window & { + __BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: { + channelName: string; + content: string; + kind?: number; + pubkey?: string; + extraTags?: string[][]; + }) => unknown; + } + ).__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({ + channelName: "general", + content: "", + kind: 20002, + pubkey, + extraTags: [["e", "mock-general-welcome", "", "reply"]], + }); + }, AGENT_PUBKEY); +} + +test.describe("process text truncation screenshots", () => { + test.use({ viewport: { width: 960, height: 720 } }); + + test.beforeEach(async ({ page }) => { + await installMockBridge(page, { + managedAgents: [ + { + pubkey: AGENT_PUBKEY, + name: AGENT_NAME, + status: "running", + channelNames: ["general"], + }, + ], + }); + await page.goto("/"); + await page.getByTestId("channel-general").click(); + await expect(page.getByTestId("chat-title")).toHaveText("general"); + }); + + test("main composer process text ellipsizes", async ({ page }) => { + await seedMainBotTyping(page); + const trigger = page.getByTestId("bot-activity-composer-trigger"); + await expect(trigger).toBeVisible(); + await expect(trigger).toContainText("Working"); + + await expect + .poll(async () => + trigger.evaluate((el) => { + const rect = el.getBoundingClientRect(); + const parent = el.parentElement?.getBoundingClientRect(); + return { + contained: parent + ? rect.left >= parent.left && rect.right <= parent.right + 1 + : false, + width: Math.round(rect.width), + parentWidth: parent ? Math.round(parent.width) : 0, + }; + }), + ) + .toMatchObject({ contained: true }); + + await waitForAnimations(page); + await page.screenshot({ + path: `${SHOTS}/01-main-composer-process-text.png`, + clip: { x: 245, y: 590, width: 690, height: 105 }, + }); + }); + + test("thread composer process text ellipsizes", async ({ page }) => { + await page.evaluate(() => { + ( + window as Window & { + __BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: { + channelName: string; + content: string; + parentEventId?: string; + }) => unknown; + } + ).__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({ + channelName: "general", + content: "Initial thread reply so the thread panel can open", + parentEventId: "mock-general-welcome", + }); + }); + + const threadSummary = page.getByTestId("message-thread-summary").first(); + await expect(threadSummary).toBeVisible(); + await threadSummary.click(); + await expect(page.getByTestId("message-thread-panel")).toBeVisible(); + + await seedThreadBotTyping(page); + const panel = page.getByTestId("message-thread-panel"); + const trigger = panel.getByTestId("bot-activity-composer-trigger"); + await expect(trigger).toBeVisible(); + await expect(trigger).toContainText("Working"); + + await expect + .poll(async () => + trigger.evaluate((el) => { + const rect = el.getBoundingClientRect(); + const parent = el.parentElement?.getBoundingClientRect(); + return { + contained: parent + ? rect.left >= parent.left && rect.right <= parent.right + 1 + : false, + width: Math.round(rect.width), + parentWidth: parent ? Math.round(parent.width) : 0, + }; + }), + ) + .toMatchObject({ contained: true }); + + await waitForAnimations(page); + await page.screenshot({ + path: `${SHOTS}/02-thread-composer-process-text.png`, + clip: { x: 525, y: 565, width: 420, height: 135 }, + }); + }); +});