Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 44 additions & 30 deletions desktop/src/features/messages/ui/MentionAutocomplete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,44 +96,58 @@ export const MentionAutocomplete = React.memo(function MentionAutocomplete({
avatarUrl={suggestion.avatarUrl ?? null}
displayName={suggestion.displayName}
size="xs"
testId="mention-suggestion-avatar"
/>
<span className="flex min-w-0 flex-1 items-center gap-2">
<span className="flex min-w-0 flex-1 items-baseline gap-1">
<span className="truncate font-medium">
{suggestion.displayName}
</span>
{suggestion.isAgent ? (
<span
className={cn(
"inline-flex shrink-0 items-center gap-1 text-xs",
index === selectedIndex
? "text-accent-foreground/70"
: "text-muted-foreground",
)}
>
<Bot
aria-hidden="true"
className="h-4 w-4"
data-testid="mention-agent-icon"
/>
{agentLabel}
</span>
) : suggestion.role ? (
<Badge variant="secondary">{suggestion.role}</Badge>
) : null}
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span
className="min-w-0 break-words font-medium leading-snug"
title={suggestion.displayName}
>
{suggestion.displayName}
</span>
{suggestion.ownerLabel || suggestion.notInChannel ? (
{suggestion.isAgent ||
suggestion.role ||
suggestion.ownerLabel ||
suggestion.notInChannel ? (
<span
className={cn(
"ml-auto min-w-0 shrink truncate text-xs",
"flex min-w-0 items-center gap-1.5 text-2xs leading-none",
index === selectedIndex
? "text-accent-foreground/65"
? "text-accent-foreground/60"
: "text-muted-foreground",
)}
>
{suggestion.ownerLabel
? `owned by ${suggestion.ownerLabel}${suggestion.notInChannel ? " · not in channel" : ""}`
: "not in channel"}
{suggestion.isAgent ? (
<span className="inline-flex shrink-0 items-center gap-1">
<Bot
aria-hidden="true"
className="h-3.5 w-3.5"
data-testid="mention-agent-icon"
/>
{agentLabel}
</span>
) : suggestion.role ? (
<Badge
className="max-w-24 shrink-0 truncate"
variant="secondary"
>
{suggestion.role}
</Badge>
) : null}
{suggestion.ownerLabel || suggestion.notInChannel ? (
<span
className="min-w-0 truncate"
title={
suggestion.ownerLabel
? `owned by ${suggestion.ownerLabel}${suggestion.notInChannel ? " · not in channel" : ""}`
: "not in channel"
}
>
{suggestion.ownerLabel
? `owned by ${suggestion.ownerLabel}${suggestion.notInChannel ? " · not in channel" : ""}`
: "not in channel"}
</span>
) : null}
</span>
) : null}
</span>
Expand Down
112 changes: 112 additions & 0 deletions desktop/tests/e2e/mentions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,46 @@ function commandCount(commands: string[], command: string) {
return commands.filter((entry) => entry === command).length;
}

async function emitMockMessage(
page: import("@playwright/test").Page,
channelName: string,
content: string,
options?: {
parentEventId?: string;
pubkey?: string;
},
) {
const event = await page.evaluate(
({ ch, msg, parentEventId, pubkey }) => {
return (
window as Window & {
__BUZZ_E2E_EMIT_MOCK_MESSAGE__?: (input: {
channelName: string;
content: string;
parentEventId?: string | null;
pubkey?: string;
}) => { id: string; created_at: number; pubkey: string };
}
).__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: ch,
content: msg,
parentEventId: parentEventId ?? undefined,
pubkey: pubkey ?? undefined,
});
},
{
ch: channelName,
msg: content,
parentEventId: options?.parentEventId ?? null,
pubkey: options?.pubkey ?? TEST_IDENTITIES.alice.pubkey,
},
);
if (!event) {
throw new Error("Mock message emitter is not installed");
}
return event;
}

async function waitForMockLiveSubscription(
page: import("@playwright/test").Page,
channelName: string,
Expand Down Expand Up @@ -135,6 +175,78 @@ test("@ trigger shows unified autocomplete with agents first", async ({
expect(bobIndex).toBeLessThan(charlieIndex);
});

test("thread autocomplete keeps multiple long names readable in a narrow panel", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey:
"9999999999999999999999999999999999999999999999999999999999999999",
name: "Brain With A Very Long Name",
status: "stopped",
},
{
pubkey:
"9999999999999999999999999999999999999999999999999999999999999998",
name: "Brainstorming Assistant With A Long Name",
status: "stopped",
},
{
pubkey:
"9999999999999999999999999999999999999999999999999999999999999997",
name: "Brainy Helper With Another Long Name",
status: "stopped",
},
],
});
await page.setViewportSize({ width: 900, height: 640 });
await page.addInitScript(() => {
window.sessionStorage.setItem("buzz.desktop.thread-panel-width", "300");
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await page.setViewportSize({ width: 760, height: 640 });

await emitMockMessage(page, "general", "Reply to open the thread", {
parentEventId: "mock-general-welcome",
});
const threadSummary = page.getByTestId("message-thread-summary").first();
await expect(threadSummary).toBeVisible();
await threadSummary.click();

const threadPanel = page.getByTestId("message-thread-panel");
await expect(threadPanel).toBeVisible();
const panelBox = await threadPanel.boundingBox();
expect(panelBox?.width ?? Number.POSITIVE_INFINITY).toBeLessThanOrEqual(320);

const input = threadPanel.getByTestId("message-input");
await input.fill("@Brain");

const dropdown = threadPanel.getByTestId("mention-autocomplete");
await expect(dropdown).toBeVisible();

for (const name of [
"Brain With A Very Long Name",
"Brainstorming Assistant With A Long Name",
"Brainy Helper With Another Long Name",
]) {
const row = dropdown.locator("button", { hasText: name });
await expect(row).toBeVisible();
await expect(
row.getByTestId("mention-suggestion-avatar-fallback"),
).toBeVisible();
await expect(row.getByText("agent")).toBeVisible();
await expect(row.getByText(/owned by npub1mock/)).toBeVisible();

await expect(row.getByText(name)).not.toHaveCSS(
"text-overflow",
"ellipsis",
);
}
});

test("autocomplete filters suggestions as user types", async ({ page }) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
Expand Down
Loading