Skip to content
Draft
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
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
18 changes: 13 additions & 5 deletions desktop/src/features/channels/ui/BotActivityBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -177,7 +177,7 @@ export function BotActivityComposerAction({
onMouseLeave={closeWithDelay}
type="button"
>
<span className="flex items-center overflow-visible py-px -space-x-1">
<span className="flex shrink-0 items-center overflow-visible py-px -space-x-1">
{typingAgents.slice(0, 2).map((agent) => (
<UserAvatar
avatarUrl={agentAvatarUrl(agent)}
Expand All @@ -193,12 +193,20 @@ export function BotActivityComposerAction({
))}
</span>
{typingAgents.length > 2 ? (
<span className="text-2xs leading-none">
<span className="shrink-0 text-2xs leading-none">
+{typingAgents.length - 2}
</span>
) : null}
<span className={cn(isInline ? "max-w-40 truncate" : "sr-only")}>
{isInline ? <Shimmer>{visibleStatusLabel}</Shimmer> : "working"}
<span
className={cn(isInline ? "min-w-0 flex-1 truncate" : "sr-only")}
>
{isInline ? (
<Shimmer className="max-w-full truncate">
{visibleStatusLabel}
</Shimmer>
) : (
"working"
)}
</span>
{isInline ? null : (
<Loader2 className="h-4 w-4 shrink-0 animate-spin opacity-70" />
Expand Down
2 changes: 1 addition & 1 deletion desktop/src/features/channels/ui/ChannelPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,7 @@ export const ChannelPane = React.memo(function ChannelPane({
<div className="h-7 overflow-visible bg-background px-5 pb-1 pt-0">
<div className="flex h-full w-full items-center gap-2 overflow-visible">
{hasComposerBotActivity ? (
<div className="shrink-0 overflow-visible">
<div className="min-w-0 flex-1 overflow-hidden">
<BotActivityComposerAction
agents={activityAgents}
channelId={activeChannel?.id ?? null}
Expand Down
12 changes: 6 additions & 6 deletions desktop/src/features/messages/ui/MessageThreadPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,9 @@ type MessageThreadPanelProps = {
onFollowThread?: () => 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 = {
Expand Down Expand Up @@ -908,7 +906,9 @@ export function MessageThreadPanel({
>
<div className="mx-auto flex h-full w-full max-w-4xl items-center gap-2">
{toolbarExtraActions ? (
<div className="shrink-0">{toolbarExtraActions}</div>
<div className="min-w-0 flex-1 overflow-hidden">
{toolbarExtraActions}
</div>
) : null}
{threadTypingPubkeys.length > 0 ? (
<TypingIndicatorRow
Expand Down
172 changes: 172 additions & 0 deletions desktop/tests/e2e/process-text-screenshots.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { expect, test, type Page } from "@playwright/test";

import { installMockBridge } from "../helpers/bridge";
import { waitForAnimations } from "../helpers/animations";

const SHOTS = "test-results/process-text";
const AGENT_PUBKEY = "ab".repeat(32);
const AGENT_NAME =
"Brain process status with a deliberately long operation label that should ellipsize cleanly";
const KIND_TYPING_INDICATOR = 20002;

async function waitForMockLiveSubscription(
Comment on lines +1 to +12

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@thomaspblock do we need the screenshot tests here for this one? It seems like it's adding code and test time, so I want to make sure it's useful.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still a draft. Will add video/screenshot once I am sure it works.

page: Page,
channelName: string,
kind?: number,
) {
await expect
.poll(async () => {
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 },
});
});
});
Loading