diff --git a/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx b/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx index a907f87245..641b81490b 100644 --- a/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx +++ b/desktop/src/features/channels/ui/AgentSessionThreadPanel.tsx @@ -35,10 +35,12 @@ import { AuxiliaryPanelHeader, AuxiliaryPanelHeaderActions, AuxiliaryPanelHeaderGroup, - AuxiliaryPanelHeaderTitleBlock, } from "@/shared/layout/AuxiliaryPanel"; import { Button } from "@/shared/ui/button"; import type { UserProfileLookup } from "@/features/profile/lib/identity"; +import { resolveUserLabel } from "@/features/profile/lib/identity"; +import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar"; +import { normalizePubkey } from "@/shared/lib/pubkey"; import { DropdownMenu, DropdownMenuContent, @@ -237,6 +239,15 @@ export function AgentSessionThreadPanel({ ? `#${scopeChannelName}` : "1 channel" : "All channels"; + const agentProfile = profiles?.[normalizePubkey(agent.pubkey)] ?? null; + const agentLabel = resolveUserLabel({ + pubkey: agent.pubkey, + fallbackName: agent.name, + profiles, + preferResolvedSelfLabel: true, + }); + const viewLabel = showRawFeed ? "Raw ACP activity" : "Activity"; + const headerScopeLabel = `${viewLabel} · ${scopeLabel}`; const animateActivity = useTranscriptAnimationEnabled(); const showTimestamps = useTranscriptTimestampsEnabled(); async function handleInterruptTurn() { @@ -417,19 +428,39 @@ export function AgentSessionThreadPanel({ backButtonTestId="agent-session-back" onBack={onBack} > - - {/* Scope label: makes channel-targeted vs all-channels state obvious - (an all-channels pane can look "wrong" without it). */} - - {scopeLabel} - +
+

+ {agentLabel} +

+
+

+ {headerScopeLabel} +

+ + + {lastUpdatedLabel} + +
+
{agentHeaderActions} diff --git a/desktop/tests/e2e/activity-scope-label-screenshots.spec.ts b/desktop/tests/e2e/activity-scope-label-screenshots.spec.ts index 0fee5ebf9d..4ed0a211e5 100644 --- a/desktop/tests/e2e/activity-scope-label-screenshots.spec.ts +++ b/desktop/tests/e2e/activity-scope-label-screenshots.spec.ts @@ -6,6 +6,8 @@ import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge"; const SHOTS = "test-results/activity-scope-label"; const AGENT_PUBKEY = TEST_IDENTITIES.tyler.pubkey; +const LONG_AGENT_NAME = + "Observer Agent With An Exceptionally Long Display Name"; const AGENTS_CHANNEL_ID = "94a444a4-c0a3-5966-ab05-530c6ddc2301"; // #agents // Open the activity pane via profile → "View activity" (same ingress the @@ -57,9 +59,36 @@ test.describe("activity panel scope label", () => { "channel-agents", "agents", ); - await expect(page.getByTestId("agent-session-scope-label")).toHaveText( - "#agents", + await expect(page.getByTestId("agent-session-agent-name")).toHaveText( + "Observer Agent", ); + await expect(page.getByTestId("agent-session-agent-avatar")).toBeVisible(); + const scope = page.getByTestId("agent-session-scope-label"); + await expect(scope).toHaveText("Activity · #agents"); + const recency = page.getByTestId("agent-session-recency-label"); + await expect(recency).toHaveText("No updates yet"); + await expect(recency).toBeVisible(); + + // Simulate long-scope pressure deterministically: scope owns truncation + // while the independently shrinking recency stays visible. + await scope.evaluate((element) => { + element.style.width = "5rem"; + element.style.flex = "none"; + }); + expect( + await scope.evaluate((element) => element.scrollWidth), + ).toBeGreaterThan(await scope.evaluate((element) => element.clientWidth)); + await expect(recency).toBeVisible(); + + await page.getByTestId("agent-session-settings-menu-trigger").click(); + await page.getByTestId("agent-session-toggle-raw-feed").click(); + await expect(scope).toHaveText("Raw ACP activity · #agents"); + expect( + await scope.evaluate((element) => element.scrollWidth), + ).toBeGreaterThan(await scope.evaluate((element) => element.clientWidth)); + await expect(recency).toHaveText("No updates yet"); + await expect(recency).toBeVisible(); + await page.keyboard.press("Escape"); await waitForAnimations(page); await panel.screenshot({ path: `${SHOTS}/01-channel-scoped.png` }); @@ -75,11 +104,18 @@ test.describe("activity panel scope label", () => { managedAgents: [ { pubkey: AGENT_PUBKEY, - name: "Observer Agent", + name: LONG_AGENT_NAME, status: "running" as const, channelNames: ["random"], }, ], + searchProfiles: [ + { + pubkey: AGENT_PUBKEY, + displayName: LONG_AGENT_NAME, + isAgent: true, + }, + ], }); await page.goto( @@ -90,9 +126,36 @@ test.describe("activity panel scope label", () => { const panel = page.getByTestId("agent-session-thread-panel"); await expect(panel).toBeVisible({ timeout: 10_000 }); - await expect(page.getByTestId("agent-session-scope-label")).toHaveText( - "All channels", + await expect(page.getByTestId("agent-session-agent-name")).toHaveText( + LONG_AGENT_NAME, + ); + const agentName = page.getByTestId("agent-session-agent-name"); + expect( + await agentName.evaluate((element) => element.scrollWidth), + ).toBeGreaterThan( + await agentName.evaluate((element) => element.clientWidth), + ); + const scope = page.getByTestId("agent-session-scope-label"); + await expect(scope).toHaveText("Activity · All channels"); + const recency = page.getByTestId("agent-session-recency-label"); + await expect(recency).toHaveText("No updates yet"); + await expect(recency).toBeVisible(); + + await page.setViewportSize({ width: 720, height: 700 }); + await expect(scope).toBeVisible(); + await expect(recency).toBeVisible(); + const recencyWidth = await recency.evaluate( + (element) => element.clientWidth, + ); + + await page.getByTestId("agent-session-settings-menu-trigger").click(); + await page.getByTestId("agent-session-toggle-raw-feed").click(); + await expect(scope).toHaveText("Raw ACP activity · All channels"); + await expect(recency).toBeVisible(); + expect(await recency.evaluate((element) => element.clientWidth)).toBe( + recencyWidth, ); + await page.keyboard.press("Escape"); await waitForAnimations(page); await panel.screenshot({ path: `${SHOTS}/02-all-channels.png` });