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
Original file line number Diff line number Diff line change
@@ -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');
});
});
7 changes: 5 additions & 2 deletions backend/routes/registry/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
34 changes: 34 additions & 0 deletions frontend/src/v2/__tests__/V2Inspector.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' }] }
Expand Down
27 changes: 17 additions & 10 deletions frontend/src/v2/components/V2Inspector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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”).
Expand All @@ -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 = (
Expand Down Expand Up @@ -202,7 +205,11 @@ const V2Inspector: React.FC<V2InspectorProps> = ({ detail, currentUserId, onPodC
{attention.map((item) => (
<button key={`${item.kind}:${item.id}`} type="button" className="v2-workspace-inspector__attention" onClick={() => openAttention(item)}>
<span>{item.title}</span>
{item.actorName && <span className="v2-workspace-inspector__actor">{labelForAttentionActor(agents, item.actorName)}</span>}
{item.actorName && (
<span className="v2-workspace-inspector__actor">
{labelForAttentionActor(agents, item.actorName, t('common.agent'))}
</span>
)}
</button>
))}
</div>
Expand Down
Loading