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.
This commit is contained in:
Mohamed Boudra
2026-10-28 13:50:16 +07:00
committed by GitHub
parent befc3d5663
commit bede2b90ed
3 changed files with 66 additions and 2 deletions
@@ -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;
+7 -1
View File
@@ -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<ViewStyle>;
textStyle?: StyleProp<TextStyle>;
numberOfLines?: number;
loading?: boolean;
}
>) {
@@ -277,7 +279,11 @@ export function Button({
iconSize={buttonIconSize[size]}
uniProps={resolveIconMapping()}
/>
{children != null ? <Text style={resolvedTextStyle}>{children}</Text> : null}
{children != null ? (
<Text numberOfLines={numberOfLines} style={resolvedTextStyle}>
{children}
</Text>
) : null}
{trailing}
</Pressable>
);
@@ -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] },