📇 feat: Agent Contact Visibility with Owner Fallback (#13663)

* Shared Contract

* Backend Resolution

* Frontend Display

* Contact Styling and more tests

* fix contact flicker when saving an agent

* fix display owner when contact deleted

* simplification of the last fixes

* github action fixes

* fixes failing tests

---------

Co-authored-by: Peter Rothlaender <peter.rothlaender@ginkgo.com>
This commit is contained in:
Peter 2026-06-25 21:58:15 +02:00 committed by GitHub
parent 376370d610
commit abf9fc307d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 894 additions and 111 deletions

View file

@ -0,0 +1,60 @@
import { resolveAgentOwnerContact } from './contact';
describe('resolveAgentOwnerContact', () => {
it('omits owner fallback when support contact has a name', () => {
const result = resolveAgentOwnerContact(
{ support_contact: { name: 'Support Team' } },
{ name: 'Agent Owner', email: 'owner@example.com' },
);
expect(result).toBeUndefined();
});
it('omits owner fallback when support contact has an email', () => {
const result = resolveAgentOwnerContact(
{ support_contact: { email: 'support@example.com' } },
{ name: 'Agent Owner', email: 'owner@example.com' },
);
expect(result).toBeUndefined();
});
it('uses owner name and email when support contact is empty', () => {
const result = resolveAgentOwnerContact(
{ support_contact: { name: ' ', email: '' } },
{ name: ' Agent Owner ', email: ' owner@example.com ' },
);
expect(result).toEqual({ name: 'Agent Owner', email: 'owner@example.com' });
});
it('falls back to username for owner display name', () => {
const result = resolveAgentOwnerContact({}, { username: 'owner.user' });
expect(result).toEqual({ name: 'owner.user' });
});
it('falls back to authorName when owner has no display name', () => {
const result = resolveAgentOwnerContact(
{ authorName: 'Legacy Author' },
{ email: 'owner@example.com' },
);
expect(result).toEqual({ name: 'Legacy Author', email: 'owner@example.com' });
});
it('omits owner contact when no owner can be resolved', () => {
const result = resolveAgentOwnerContact({ authorName: 'Legacy Author' }, null);
expect(result).toBeUndefined();
});
it('omits owner contact when all candidate fields are empty', () => {
const result = resolveAgentOwnerContact(
{ authorName: ' ' },
{ name: '', username: ' ', email: ' ' },
);
expect(result).toBeUndefined();
});
});

View file

@ -0,0 +1,55 @@
import type { AgentOwnerContact } from 'librechat-data-provider';
export interface AgentContactSource {
authorName?: string | null;
support_contact?: {
name?: string | null;
email?: string | null;
} | null;
}
export interface AgentOwnerContactSource {
name?: string | null;
username?: string | null;
email?: string | null;
}
const normalizeContactValue = (value?: string | null): string | undefined => {
if (typeof value !== 'string') {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
};
export const hasSupportContact = (agent: AgentContactSource): boolean => {
const support = agent.support_contact;
if (!support) {
return false;
}
return !!normalizeContactValue(support.name) || !!normalizeContactValue(support.email);
};
export function resolveAgentOwnerContact(
agent: AgentContactSource,
owner: AgentOwnerContactSource | null,
): AgentOwnerContact | undefined {
if (hasSupportContact(agent) || owner == null) {
return undefined;
}
const name =
normalizeContactValue(owner.name) ??
normalizeContactValue(owner.username) ??
normalizeContactValue(agent.authorName);
const email = normalizeContactValue(owner.email);
if (!name && !email) {
return undefined;
}
return {
...(name ? { name } : {}),
...(email ? { email } : {}),
};
}

View file

@ -3,6 +3,7 @@ export * from './attachments';
export * from './chain';
export * from './client';
export * from './config';
export * from './contact';
export * from './context';
export * from './discovery';
export * from './edges';