diff --git a/backend/__tests__/unit/routes/registry.roster-display-label.test.js b/backend/__tests__/unit/routes/registry.roster-display-label.test.js new file mode 100644 index 0000000000..b7cab20b25 --- /dev/null +++ b/backend/__tests__/unit/routes/registry.roster-display-label.test.js @@ -0,0 +1,37 @@ +/** + * TASK-551: the roster never labels a seat with its agentName. With a User row + * the label is the chat label. Without one, only a scoped name that differs + * from the agentName counts; otherwise the label is '' and the client shows its + * generic fallback. + */ + +const { buildAgentInstallationPayload } = require('../../../routes/registry/helpers'); + +const install = { agentName: 'scout', instanceId: 'ubd1004e0d7', status: 'active', scopes: [], createdAt: new Date() }; + +describe('roster displayName for an agent with no User row', () => { + test('is empty when no curated name is set, never the agentName', () => { + expect(buildAgentInstallationPayload(install).displayName).toBe(''); + }); + + test('is empty when the installation displayName is only the agentName', () => { + expect(buildAgentInstallationPayload({ ...install, displayName: 'scout' }).displayName).toBe(''); + }); + + test('is empty when the profile name is only the agentName', () => { + expect(buildAgentInstallationPayload(install, { profile: { name: 'scout' } }).displayName).toBe(''); + }); + + test('falls past an agentName-equal profile name to a curated installation name', () => { + const payload = buildAgentInstallationPayload( + { ...install, displayName: 'Scout Desk' }, + { profile: { name: 'scout' } }, + ); + expect(payload.displayName).toBe('Scout Desk'); + }); + + test('keeps a curated name that differs from the agentName', () => { + expect(buildAgentInstallationPayload({ ...install, displayName: 'Scout' }).displayName).toBe('Scout'); + expect(buildAgentInstallationPayload(install, { profile: { name: 'Scout Desk' } }).displayName).toBe('Scout Desk'); + }); +}); diff --git a/backend/routes/registry/helpers.ts b/backend/routes/registry/helpers.ts index 77a95fff77..5ea9eba14d 100644 --- a/backend/routes/registry/helpers.ts +++ b/backend/routes/registry/helpers.ts @@ -477,15 +477,18 @@ const buildAgentInstallationPayload = (installation: any, { } // Chat attributes this seat through the User principal. Use that same // resolver for the listing; scoped names are only a fallback for installs - // that have no User row yet. + // that have no User row yet. A scoped name equal to the agentName is the + // runtime or type label, not a name, so the listing never carries it (TASK-551). const profileDisplayName = typeof profile?.name === 'string' ? profile.name.trim() : ''; const installationDisplayName = typeof installation.displayName === 'string' ? installation.displayName.trim() : ''; const identityFallback = installation.agentName || ''; + const scopedDisplayName = [profileDisplayName, installationDisplayName] + .find((name) => name && name !== identityFallback) || ''; const displayName = user ? resolveAgentDisplayLabel(user, identityFallback) - : profileDisplayName || installationDisplayName || identityFallback; + : scopedDisplayName; return { name: installation.agentName, instanceId: installation.instanceId || 'default', diff --git a/frontend/src/v2/__tests__/V2Inspector.test.tsx b/frontend/src/v2/__tests__/V2Inspector.test.tsx index 88b54cebad..ad0d150f4f 100644 --- a/frontend/src/v2/__tests__/V2Inspector.test.tsx +++ b/frontend/src/v2/__tests__/V2Inspector.test.tsx @@ -195,6 +195,40 @@ describe('V2Inspector', () => { expect(screen.getByRole('button', { name: /Scout idle/ })).toBeInTheDocument(); }); + test('names an attention item by the agent it matches, never by its agentName or instance id', () => { + renderInspector({ detail: { ...detail, agents: [{ + agentName: 'scout', instanceId: 'ubd1004e0d7', displayName: 'Scout', + }] } as any, attentionItems: [{ + id: 'scout-request', kind: 'decision', title: 'Scout request', + actorName: 'scout', podId: 'pod-1', + }] }); + + expect(document.querySelector('.v2-workspace-inspector__actor')?.textContent).toBe('Scout'); + expect(screen.queryByText('scout')).not.toBeInTheDocument(); + expect(screen.queryByText('ubd1004e0d7')).not.toBeInTheDocument(); + }); + + test('names an unmatched attention actor generically, never by its raw id', () => { + renderInspector({ detail: { ...detail, agents: [{ + agentName: 'scout', instanceId: 'ubd1004e0d7', displayName: 'Scout', + }] } as any, attentionItems: [{ + id: 'orphan-request', kind: 'decision', title: 'Orphan request', + actorName: 'u1a9e5ecede', podId: 'pod-1', + }] }); + + expect(document.querySelector('.v2-workspace-inspector__actor')?.textContent).toBe('Agent'); + expect(screen.queryByText('u1a9e5ecede')).not.toBeInTheDocument(); + }); + + test('labels an agent whose profile name is only its agentName generically', () => { + renderInspector({ detail: { ...detail, agents: [{ + agentName: 'scout', instanceId: 'ubd1004e0d7', profile: { displayName: 'scout' }, + }] } as any, attentionCount: 0, attentionItems: [] }); + + expect(screen.getByRole('button', { name: /Agent idle/ })).toBeInTheDocument(); + expect(screen.queryByText('scout')).not.toBeInTheDocument(); + }); + test('does not assign an unowned active task to an agent without a profile name', async () => { mockGet.mockImplementation((url: string) => Promise.resolve(url === '/api/v1/tasks/pod-1' ? { tasks: [{ taskId: 'TASK-1', title: 'Unowned work', status: 'claimed' }] } diff --git a/frontend/src/v2/components/V2Inspector.tsx b/frontend/src/v2/components/V2Inspector.tsx index aac411c544..985b19c95e 100644 --- a/frontend/src/v2/components/V2Inspector.tsx +++ b/frontend/src/v2/components/V2Inspector.tsx @@ -48,15 +48,18 @@ const isWorkingTask = (task: TaskItem): boolean => ( ); -const labelForAgent = (agent: V2Agent, fallback: string): string => ( +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. Each - // caller passes its own generic fallback, so no label renders blank. - agent.displayName || agent.profile?.displayName || fallback -); + // caller passes its own generic fallback, so no label renders blank. A + // profile name equal to the agentName is that same name, so it is skipped. + const profileName = agent.profile?.displayName || ''; + const profileLabel = profileName === (agent.agentName || '') ? '' : profileName; + return agent.displayName || profileLabel || fallback; +}; // Pods often carry a longer descriptive title, while the compact inspector // heading uses the room's short leading name (for example, “Sharpen”). @@ -74,11 +77,11 @@ const matchesAgent = (agent: V2Agent, value?: string): boolean => { || candidate === (agent.instanceId || '').toLowerCase(); }; -const labelForAttentionActor = (agents: V2Agent[], actorName: string): string => { - const legacyAgent = agents.find((agent) => ( - agent.profile?.displayName?.toLowerCase() === actorName.toLowerCase() - )); - return legacyAgent ? labelForAgent(legacyAgent, actorName) : actorName; +// An attention actor is an agentName. Match it the way agentState does, then +// name that agent. An unmatched actor gets the generic label, never its raw name. +const labelForAttentionActor = (agents: V2Agent[], actorName: string, fallback: string): string => { + const agent = agents.find((candidate) => matchesAgent(candidate, actorName)); + return agent ? labelForAgent(agent, fallback) : fallback; }; const agentState = ( @@ -202,7 +205,11 @@ const V2Inspector: React.FC = ({ detail, currentUserId, onPodC {attention.map((item) => ( ))}