From bace6cdd60e1522be685ad2957ceb82031787bb1 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 30 Oct 2026 02:27:40 -0700 Subject: [PATCH 1/2] fix(inspector): never label an agent by its agentName (TASK-551) The My Workspace roster takes its label from the resolved displayName. The inspector fell back to agentName, the runtime or type name, when that was empty. It now falls back to the generic "Agent" label instead. The raw id on the Scout row is the backend leak rule in resolveAgentDisplayLabel, which returns the instance id for a curated label that equals the agent name. That fix is commit 30719539 on swift/task-550-551-labels (PR #2737) and is not in this change. Adds render tests: a per-user Scout shows its name and never its instance id or agentName, and an agent with no roster name shows the generic label. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: wren Agent-Model: claude/claude-haiku-5-5/max --- .../src/v2/__tests__/V2Inspector.test.tsx | 19 +++++++++++++++++++ frontend/src/v2/components/V2Inspector.tsx | 5 +++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/frontend/src/v2/__tests__/V2Inspector.test.tsx b/frontend/src/v2/__tests__/V2Inspector.test.tsx index f64389bad7..98b235deda 100644 --- a/frontend/src/v2/__tests__/V2Inspector.test.tsx +++ b/frontend/src/v2/__tests__/V2Inspector.test.tsx @@ -155,6 +155,25 @@ describe('V2Inspector', () => { expect(screen.queryByText('u1a9e5ecede')).not.toBeInTheDocument(); }); + test('labels a per-user agent by its resolved name, never its instance id or agentName', () => { + renderInspector({ detail: { ...detail, agents: [{ + agentName: 'scout', instanceId: 'ubd1004e0d7', displayName: 'Scout', + }] } as any, attentionCount: 0, attentionItems: [] }); + + expect(screen.getByRole('button', { name: /Scout idle/ })).toBeInTheDocument(); + expect(screen.queryByText('ubd1004e0d7')).not.toBeInTheDocument(); + expect(screen.queryByText('scout')).not.toBeInTheDocument(); + }); + + test('labels an agent with no roster name generically, never by its runtime name', () => { + renderInspector({ detail: { ...detail, agents: [{ + agentName: 'openclaw', instanceId: 'pixel', + }] } as any, attentionCount: 0, attentionItems: [] }); + + expect(screen.getByRole('button', { name: /Agent idle/ })).toBeInTheDocument(); + expect(screen.queryByText('openclaw')).not.toBeInTheDocument(); + }); + test('does not assign anonymous attention to an agent without a profile name', () => { renderInspector({ detail: { ...detail, agents: [{ agentName: 'scout', instanceId: 'default', displayName: 'Scout', diff --git a/frontend/src/v2/components/V2Inspector.tsx b/frontend/src/v2/components/V2Inspector.tsx index 3a7fd04bb6..0f4d9787dc 100644 --- a/frontend/src/v2/components/V2Inspector.tsx +++ b/frontend/src/v2/components/V2Inspector.tsx @@ -51,7 +51,8 @@ const isWorkingTask = (task: TaskItem): boolean => ( const labelForAgent = (agent: V2Agent): string => ( // The roster's displayName is resolved from the agent User identity. A // profile name may be an older generated ID, so it is only a fallback. - agent.displayName || agent.profile?.displayName || agent.agentName + // agentName is never a label: it names the runtime or the agent type. + agent.displayName || agent.profile?.displayName || '' ); // Pods often carry a longer descriptive title, while the compact inspector @@ -210,7 +211,7 @@ const V2Inspector: React.FC = ({ detail, currentUserId, onPodC
{agents.length === 0 &&

{t('inspector.workspace.noAgents')}

} {agents.map((agent) => { - const name = labelForAgent(agent); + const name = labelForAgent(agent) || t('common.agent'); const username = buildAgentUsername(rawAgentName(agent), agent.instanceId); const member = detail.members.find((row) => row.username?.toLowerCase() === username); const state = agentState(agent, attention, tasks); From 8dd6eb3638e70b981dad992dffe6d0aa04aa9fad Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Fri, 30 Oct 2026 02:45:01 -0700 Subject: [PATCH 2/2] fix(inspector): keep a nameless agent from blanking the working copy (TASK-551) labelForAgent returned '' for an agent with no roster name, and the working copy joined that bare, so the empty-state line read "Nothing. is working." Each caller now passes its own generic fallback. Matching passes '' so the generic text cannot match an attention item. The chain comment now says why it stops at the roster displayName. Adds a render test for a working agent with no name, which fails on bace6cdd6. Co-Authored-By: Claude Haiku 5.5 Co-authored-by: wren Agent-Model: claude/claude-haiku-5-5/max --- .../src/v2/__tests__/V2Inspector.test.tsx | 10 ++++++++++ frontend/src/v2/components/V2Inspector.tsx | 20 +++++++++++-------- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/frontend/src/v2/__tests__/V2Inspector.test.tsx b/frontend/src/v2/__tests__/V2Inspector.test.tsx index 98b235deda..88b54cebad 100644 --- a/frontend/src/v2/__tests__/V2Inspector.test.tsx +++ b/frontend/src/v2/__tests__/V2Inspector.test.tsx @@ -174,6 +174,16 @@ describe('V2Inspector', () => { expect(screen.queryByText('openclaw')).not.toBeInTheDocument(); }); + test('names a working agent with no roster name generically, never with an empty label', async () => { + mockGet.mockResolvedValue({ tasks: [] }); + renderInspector({ detail: { ...detail, agents: [{ + agentName: 'openclaw', instanceId: 'pixel', status: 'working', + }] } as any, attentionCount: 0, attentionItems: [] }); + + expect(await screen.findByText('Nothing. Agent is working.')).toBeInTheDocument(); + expect(screen.queryByText(/openclaw/)).not.toBeInTheDocument(); + }); + test('does not assign anonymous attention to an agent without a profile name', () => { renderInspector({ detail: { ...detail, agents: [{ agentName: 'scout', instanceId: 'default', displayName: 'Scout', diff --git a/frontend/src/v2/components/V2Inspector.tsx b/frontend/src/v2/components/V2Inspector.tsx index 0f4d9787dc..aac411c544 100644 --- a/frontend/src/v2/components/V2Inspector.tsx +++ b/frontend/src/v2/components/V2Inspector.tsx @@ -48,11 +48,14 @@ const isWorkingTask = (task: TaskItem): boolean => ( ); -const labelForAgent = (agent: V2Agent): string => ( - // The roster's displayName is resolved from the agent User identity. A +const labelForAgent = (agent: V2Agent, fallback: string): string => ( + // The roster's displayName already went through resolveAgentDisplayLabel on + // the backend for agents with a User row. This chain stops there on purpose: + // instanceId and username are raw identifiers, which TASK-551 reports. A // profile name may be an older generated ID, so it is only a fallback. - // agentName is never a label: it names the runtime or the agent type. - agent.displayName || agent.profile?.displayName || '' + // agentName is never a label: it names the runtime or the agent type. Each + // caller passes its own generic fallback, so no label renders blank. + agent.displayName || agent.profile?.displayName || fallback ); // Pods often carry a longer descriptive title, while the compact inspector @@ -64,8 +67,9 @@ const shortRoomName = (name: string): string => ( const matchesAgent = (agent: V2Agent, value?: string): boolean => { const candidate = (value || '').trim().toLowerCase(); if (!candidate) return false; + // Matching never uses the generic label: '' keeps it out of the comparison. return candidate === (agent.agentName || '').toLowerCase() - || candidate === labelForAgent(agent).toLowerCase() + || candidate === labelForAgent(agent, '').toLowerCase() || candidate === (agent.profile?.displayName || '').toLowerCase() || candidate === (agent.instanceId || '').toLowerCase(); }; @@ -74,7 +78,7 @@ const labelForAttentionActor = (agents: V2Agent[], actorName: string): string => const legacyAgent = agents.find((agent) => ( agent.profile?.displayName?.toLowerCase() === actorName.toLowerCase() )); - return legacyAgent ? labelForAgent(legacyAgent) : actorName; + return legacyAgent ? labelForAgent(legacyAgent, actorName) : actorName; }; const agentState = ( @@ -172,7 +176,7 @@ const V2Inspector: React.FC = ({ detail, currentUserId, onPodC const workingAgents = agents .filter((agent) => agentState(agent, attention, tasks).kind === 'working') - .map(labelForAgent); + .map((agent) => labelForAgent(agent, t('common.agent'))); const emptyAttentionCopy = workingAgents.length > 0 ? t('inspector.workspace.nothingWorking', { agents: workingAgents.join(' and '), count: workingAgents.length }) : t('inspector.workspace.nothingOpen'); @@ -211,7 +215,7 @@ const V2Inspector: React.FC = ({ detail, currentUserId, onPodC
{agents.length === 0 &&

{t('inspector.workspace.noAgents')}

} {agents.map((agent) => { - const name = labelForAgent(agent) || t('common.agent'); + const name = labelForAgent(agent, t('common.agent')); const username = buildAgentUsername(rawAgentName(agent), agent.instanceId); const member = detail.members.find((row) => row.username?.toLowerCase() === username); const state = agentState(agent, attention, tasks);