From bede2b90edaeeb9f032c72ebfc267b3c0b5027b0 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 28 Oct 2026 13:50:16 +0700 Subject: [PATCH] fix(app): truncate agent sender attribution to one line (#8092) Constrain the sender label to the message width so long agent names do not wrap above received messages. Keep the line limit opt-in on Button and cover compact chat widths with a real-browser regression. --- .../app/e2e/browser/turn-collapse.spec.ts | 55 +++++++++++++++++++ packages/app/src/components/ui/button.tsx | 8 ++- .../app/src/components/user-message/index.tsx | 5 +- 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/packages/app/e2e/browser/turn-collapse.spec.ts b/packages/app/e2e/browser/turn-collapse.spec.ts index ddbff1e2f3..d990e33009 100644 --- a/packages/app/e2e/browser/turn-collapse.spec.ts +++ b/packages/app/e2e/browser/turn-collapse.spec.ts @@ -203,6 +203,47 @@ test("received messages show their sender and open that agent", async ({ page }) } }); +test("long sender attribution stays on one line in compact chat", async ({ page }, testInfo) => { + await page.setViewportSize({ width: 390, height: 844 }); + await page.addInitScript(() => + localStorage.setItem("@paseo:app-settings", JSON.stringify({ theme: "dark" })), + ); + const recipient = await seedMockAgentWorkspace({ + repoPrefix: "compact-attribution-", + title: "Implementation", + }); + try { + const title = "Reviewer checking the compact chat timeline and sender attribution"; + const sender = await recipient.client.createAgent({ + provider: "codex", + cwd: recipient.cwd, + workspaceId: recipient.workspaceId, + title, + model: "gpt-6.1-sol", + modeId: "full-access", + }); + await recipient.client.sendAgentMessage( + recipient.agentId, + "The sender label stays on one line, even when the agent has a long name.", + { sourceAgentId: sender.id }, + ); + await openAgentRoute(page, recipient); + await expectComposerVisible(page); + const attribution = page.getByRole("button", { name: `Sent by ${title}`, exact: true }); + await expect(attribution).toBeVisible(); + await expectSenderTruncatedWithinMessage(attribution, title); + await expectSenderAlignedWithBubble(attribution); + await attribution.scrollIntoViewIfNeeded(); + await page.screenshot({ path: testInfo.outputPath("sender-compact-390.png") }); + await page.setViewportSize({ width: 320, height: 740 }); + await expectSenderTruncatedWithinMessage(attribution, title); + await expectSenderAlignedWithBubble(attribution); + await page.screenshot({ path: testInfo.outputPath("sender-compact-320.png") }); + } finally { + await recipient.cleanup(); + } +}); + interface MessagePreviewWatch { /** Frames after the watch started on which the fade or "Show more" control was missing. */ framesWithoutClip: number[]; @@ -316,6 +357,20 @@ test("opening a turn keeps the received message above it clipped on every frame" } }); +async function expectSenderTruncatedWithinMessage(attribution: Locator, title: string) { + const label = attribution.getByText(`Sent by ${title}`, { exact: true }); + await expect(label).toHaveCSS("white-space", "nowrap"); + await expect(label).toHaveCSS("text-overflow", "ellipsis"); + const bounds = await label.evaluate((element) => ({ + textWidth: element.scrollWidth, + visibleWidth: element.clientWidth, + right: element.getBoundingClientRect().right, + viewportWidth: document.documentElement.clientWidth, + })); + expect(bounds.textWidth).toBeGreaterThan(bounds.visibleWidth); + expect(bounds.right).toBeLessThanOrEqual(bounds.viewportWidth); +} + async function expectSenderAlignedWithBubble(attribution: Locator) { const edges = await attribution.evaluate((button) => { const bubble = button.nextElementSibling; diff --git a/packages/app/src/components/ui/button.tsx b/packages/app/src/components/ui/button.tsx index abcaf240d6..e4a9a13b2b 100644 --- a/packages/app/src/components/ui/button.tsx +++ b/packages/app/src/components/ui/button.tsx @@ -168,6 +168,7 @@ export function Button({ trailing, style, textStyle, + numberOfLines, disabled, loading = false, accessibilityRole, @@ -182,6 +183,7 @@ export function Button({ trailing?: ReactNode; style?: StyleProp; textStyle?: StyleProp; + numberOfLines?: number; loading?: boolean; } >) { @@ -277,7 +279,11 @@ export function Button({ iconSize={buttonIconSize[size]} uniProps={resolveIconMapping()} /> - {children != null ? {children} : null} + {children != null ? ( + + {children} + + ) : null} {trailing} ); diff --git a/packages/app/src/components/user-message/index.tsx b/packages/app/src/components/user-message/index.tsx index 5f5eef4ba6..e7bef2d177 100644 --- a/packages/app/src/components/user-message/index.tsx +++ b/packages/app/src/components/user-message/index.tsx @@ -80,6 +80,8 @@ export function MessageSender({ size="sm" leftIcon={renderProviderIcon} style={styles.sender} + textStyle={styles.senderText} + numberOfLines={1} disabled={!host} onPress={openSender} > @@ -90,7 +92,8 @@ export function MessageSender({ const styles = StyleSheet.create((theme) => ({ body: { flexShrink: 0 }, - sender: { paddingRight: 0, borderRightWidth: 0 }, + sender: { paddingRight: 0, borderRightWidth: 0, maxWidth: "100%" }, + senderText: { flexShrink: 1 }, toggle: { alignSelf: "flex-start", paddingHorizontal: 0, minHeight: 0, borderWidth: 0 }, expandedToggle: { marginTop: theme.spacing[2] }, fade: { position: "absolute", bottom: 0, left: 0, right: 0, height: theme.spacing[12] },