fix(FN-3886): resolve peer agent names in mail and notifications

- Resolve participant display names in AgentDetailView mail tab for peer agents
- Update MailboxModal labeling to use readable agent names instead of raw IDs
- Propagate resolved agent names through notification service, notifier, and provider payloads
- Add and update dashboard/engine tests plus a changeset for @runfusion/fusion

Fusion-Task-Id: FN-3886
This commit is contained in:
Fusion
2026-05-09 14:58:53 -07:00
committed by gsxdsm
parent 7d3b22616c
commit 2d3af5b6ce
14 changed files with 396 additions and 37 deletions

View File

@@ -1211,9 +1211,17 @@ function formatMailboxTimestamp(ts: string): string {
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
}
function mailboxParticipantLabel(id: string, type: ParticipantType): string {
function mailboxParticipantLabel(
id: string,
type: ParticipantType,
agentNamesById?: ReadonlyMap<string, string>,
): string {
if (type === "user") return id === "dashboard" ? "You" : `User: ${id}`;
if (type === "agent") return `Agent: ${id}`;
if (type === "agent") {
const name = agentNamesById?.get(id)?.trim();
if (!name || name === id) return `Agent: ${id}`;
return `Agent: ${name}`;
}
return "System";
}
@@ -1235,6 +1243,43 @@ function MailTab({
onRefresh: () => void;
}) {
const [activeSubtab, setActiveSubtab] = useState<"inbox" | "outbox">("inbox");
const [knownAgents, setKnownAgents] = useState<Agent[]>([]);
useEffect(() => {
let cancelled = false;
fetchAgents(undefined, projectId)
.then((agents) => {
if (!cancelled) {
setKnownAgents(agents);
}
})
.catch(() => {
if (!cancelled) {
setKnownAgents([]);
}
});
return () => {
cancelled = true;
};
}, [projectId]);
const agentNamesById = useMemo(() => {
const map = new Map<string, string>();
for (const knownAgent of knownAgents) {
if (!knownAgent.id) continue;
const name = typeof knownAgent.name === "string" ? knownAgent.name.trim() : "";
if (name.length > 0) {
map.set(knownAgent.id, name);
}
}
const currentAgentName = typeof agent.name === "string" ? agent.name.trim() : "";
if (currentAgentName.length > 0) {
map.set(agent.id, currentAgentName);
}
return map;
}, [knownAgents, agent.id, agent.name]);
const [selectedMessageId, setSelectedMessageId] = useState<string | null>(null);
const messages = activeSubtab === "inbox" ? (mailbox?.inbox ?? []) : (mailbox?.outbox ?? []);
const selectedMessage = selectedMessageId ? messages.find((message) => message.id === selectedMessageId) ?? null : null;
@@ -1282,9 +1327,9 @@ function MailTab({
<div className="mailbox-item-content">
<div className="mailbox-item-header">
{activeSubtab === "inbox" ? (
<span className="mailbox-item-from">{mailboxParticipantLabel(message.fromId, message.fromType)}</span>
<span className="mailbox-item-from">{mailboxParticipantLabel(message.fromId, message.fromType, agentNamesById)}</span>
) : (
<span className="mailbox-item-to">To: {mailboxParticipantLabel(message.toId, message.toType)}</span>
<span className="mailbox-item-to">To: {mailboxParticipantLabel(message.toId, message.toType, agentNamesById)}</span>
)}
<span className="mailbox-item-time">{formatMailboxTimestamp(message.createdAt)}</span>
</div>
@@ -1351,11 +1396,11 @@ function MailTab({
<div className="agent-mail-tab-detail-meta">
<div className="agent-mail-tab-detail-row">
<span className="agent-mail-tab-detail-label">From</span>
<span>{mailboxParticipantLabel(selectedMessage.fromId, selectedMessage.fromType)}</span>
<span>{mailboxParticipantLabel(selectedMessage.fromId, selectedMessage.fromType, agentNamesById)}</span>
</div>
<div className="agent-mail-tab-detail-row">
<span className="agent-mail-tab-detail-label">To</span>
<span>{mailboxParticipantLabel(selectedMessage.toId, selectedMessage.toType)}</span>
<span>{mailboxParticipantLabel(selectedMessage.toId, selectedMessage.toType, agentNamesById)}</span>
</div>
<div className="agent-mail-tab-detail-row">
<span className="agent-mail-tab-detail-label">Type</span>

View File

@@ -68,9 +68,17 @@ function formatTimestamp(ts: string): string {
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
}
function participantLabel(id: string, type: ParticipantType): string {
function participantLabel(
id: string,
type: ParticipantType,
agentNamesById?: ReadonlyMap<string, string>,
): string {
if (type === "user") return id === "dashboard" ? "You" : `User: ${id}`;
if (type === "agent") return `Agent: ${id}`;
if (type === "agent") {
const name = agentNamesById?.get(id)?.trim();
if (!name || name === id) return `Agent: ${id}`;
return `Agent: ${name}`;
}
return "System";
}
@@ -160,6 +168,18 @@ export function MailboxModal({
const [replyContextLoading, setReplyContextLoading] = useState<Record<string, boolean>>({});
const [replyContextErrors, setReplyContextErrors] = useState<Record<string, string>>({});
const [replyContextCache, setReplyContextCache] = useState<Map<string, Message>>(new Map());
const agentNamesById = useMemo(() => {
const map = new Map<string, string>();
for (const agent of agents) {
if (!agent.id) continue;
const name = typeof agent.name === "string" ? agent.name.trim() : "";
if (name.length > 0) {
map.set(agent.id, name);
}
}
return map;
}, [agents]);
const viewportMode = useViewportMode();
const isMobile = viewportMode === "mobile";
const { keyboardOverlap, viewportHeight, viewportOffsetTop, keyboardOpen } = useMobileKeyboard({ enabled: isMobile });
@@ -532,7 +552,7 @@ export function MailboxModal({
{cacheMessage && (
<>
<div className="mailbox-conversation-msg-header">
<span>{participantLabel(cacheMessage.fromId, cacheMessage.fromType)}</span>
<span>{participantLabel(cacheMessage.fromId, cacheMessage.fromType, agentNamesById)}</span>
<span className="mailbox-message-time">{formatTimestamp(cacheMessage.createdAt)}</span>
</div>
<div className="mailbox-conversation-msg-body">{cacheMessage.content}</div>
@@ -693,14 +713,14 @@ export function MailboxModal({
<span className="mailbox-participant-label">From:</span>
<span className="mailbox-participant-value">
{selectedMessage.fromType === "agent" ? <Bot size={14} /> : <User size={14} />}
{participantLabel(selectedMessage.fromId, selectedMessage.fromType)}
{participantLabel(selectedMessage.fromId, selectedMessage.fromType, agentNamesById)}
</span>
</div>
<div className="mailbox-participant">
<span className="mailbox-participant-label">To:</span>
<span className="mailbox-participant-value">
{selectedMessage.toType === "agent" ? <Bot size={14} /> : <User size={14} />}
{participantLabel(selectedMessage.toId, selectedMessage.toType)}
{participantLabel(selectedMessage.toId, selectedMessage.toType, agentNamesById)}
</span>
</div>
</div>
@@ -721,7 +741,7 @@ export function MailboxModal({
className={`mailbox-conversation-msg ${msg.id === selectedMessage.id ? "current" : ""}`}
>
<div className="mailbox-conversation-msg-header">
<span>{participantLabel(msg.fromId, msg.fromType)}</span>
<span>{participantLabel(msg.fromId, msg.fromType, agentNamesById)}</span>
<span className="mailbox-message-time">{formatTimestamp(msg.createdAt)}</span>
</div>
{replyToId && (
@@ -804,7 +824,7 @@ export function MailboxModal({
<div className="mailbox-item-content">
<div className="mailbox-item-header">
<span className="mailbox-item-from">
{participantLabel(msg.fromId, msg.fromType)}
{participantLabel(msg.fromId, msg.fromType, agentNamesById)}
</span>
<span className="mailbox-item-time">{formatTimestamp(msg.createdAt)}</span>
</div>
@@ -840,7 +860,7 @@ export function MailboxModal({
<div className="mailbox-item-content">
<div className="mailbox-item-header">
<span className="mailbox-item-to">
To: {participantLabel(msg.toId, msg.toType)}
To: {participantLabel(msg.toId, msg.toType, agentNamesById)}
</span>
<span className="mailbox-item-time">{formatTimestamp(msg.createdAt)}</span>
</div>
@@ -945,9 +965,7 @@ export function MailboxModal({
<div className="mailbox-item-content">
<div className="mailbox-item-header">
<span className="mailbox-item-from">
{msg.fromType === "agent"
? participantLabel(msg.toId, msg.toType)
: participantLabel(msg.fromId, msg.fromType)}
{participantLabel(msg.fromId, msg.fromType, agentNamesById)}
</span>
<span className="mailbox-item-time">{formatTimestamp(msg.createdAt)}</span>
</div>
@@ -969,7 +987,7 @@ export function MailboxModal({
<div className="mailbox-item-content">
<div className="mailbox-item-header">
<span className="mailbox-item-to">
To: {participantLabel(msg.toId, msg.toType)}
To: {participantLabel(msg.toId, msg.toType, agentNamesById)}
</span>
<span className="mailbox-item-time">{formatTimestamp(msg.createdAt)}</span>
</div>

View File

@@ -867,6 +867,52 @@ describe("AgentDetailView", () => {
expect(await screen.findByTestId("agent-detail-mail-list")).toBeInTheDocument();
});
it("renders agent mailbox participant labels with known agent names", async () => {
const user = userEvent.setup();
mockFetchAgentMailbox.mockResolvedValue({
ownerId: "agent-001",
ownerType: "agent",
unreadCount: 1,
messages: [],
inbox: [
{
id: "msg-1",
fromId: "agent-001",
fromType: "agent",
toId: "dashboard",
toType: "user",
content: "Self message",
type: "agent-to-user",
createdAt: "2024-01-01T00:00:00.000Z",
updatedAt: "2024-01-01T00:00:00.000Z",
read: true,
},
],
outbox: [
{
id: "msg-2",
fromId: "agent-001",
fromType: "agent",
toId: "agent-002",
toType: "agent",
content: "Known recipient",
type: "agent-to-agent",
createdAt: "2024-01-01T00:00:00.000Z",
updatedAt: "2024-01-01T00:00:00.000Z",
read: true,
},
],
} as any);
render(<AgentDetailView agentId="agent-001" onClose={vi.fn()} addToast={vi.fn()} />);
await user.click(await screen.findByText("Mail"));
expect(await screen.findByText("Agent: Test Agent")).toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Outbox" }));
expect(await screen.findByText("To: Agent: Manager Agent")).toBeInTheDocument();
});
it("marks unread inbox messages as read and refreshes mailbox", async () => {
const user = userEvent.setup();
mockFetchAgentMailbox.mockResolvedValue({

View File

@@ -207,6 +207,42 @@ describe("MailboxModal", () => {
});
});
it("renders agent participant labels with name and id, then falls back to id", async () => {
mockFetchInbox.mockResolvedValue({
messages: [
{ ...mockMessage, id: "msg-known", fromId: "agent-001" },
{ ...mockMessage, id: "msg-unknown", fromId: "agent-999" },
],
total: 2,
unreadCount: 2,
});
render(<MailboxModal {...defaultProps} />);
await waitFor(() => {
expect(screen.getByTestId("mailbox-item-msg-known")).toBeDefined();
});
fireEvent.click(screen.getByTestId("mailbox-item-msg-known"));
await waitFor(() => {
expect(screen.getByText("Agent: Test Agent 1")).toBeDefined();
});
fireEvent.click(screen.getByTestId("mailbox-back-to-list"));
fireEvent.click(screen.getByTestId("mailbox-item-msg-unknown"));
await waitFor(() => {
expect(screen.getByText("Agent: agent-999")).toBeDefined();
});
});
it("shows unread dot for unread messages", async () => {
render(<MailboxModal {...defaultProps} />);
await waitFor(() => {
expect(screen.getByTestId("mailbox-item-msg-001")).toBeDefined();
expect(screen.getByTestId("mailbox-item-msg-002")).toBeDefined();
});
});
it("shows unread dot for unread messages", async () => {
render(<MailboxModal {...defaultProps} />);
await waitFor(() => {
@@ -745,7 +781,7 @@ describe("MailboxModal", () => {
expect(screen.getByTestId("message-composer")).toBeDefined();
});
// Should show pre-filled recipient (not dropdown)
expect(screen.getByText("agent-001")).toBeDefined();
expect(screen.getByText("Test Agent 1")).toBeDefined();
});
it("successful send from Agents tab keeps user on Agents tab and preserves selected agent", async () => {
@@ -1055,8 +1091,8 @@ describe("MailboxModal", () => {
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-title");
expect(mailboxMobileSection).toContain("flex-shrink: 0;");
expect(mailboxMobileSection).toMatch(/\.mailbox-modal \.mailbox-header-actions,\s*\.mailbox-view \.mailbox-header-actions\s*\{[^}]*gap:\s*var\(--space-sm\);[^}]*\}/);
expect(mailboxMobileSection).toMatch(/\.mailbox-modal \.mailbox-header-actions \.btn,[^}]*\.mailbox-view \.mailbox-header-actions \.btn-icon\s*\{[^}]*min-height:\s*36px;[^}]*\}/);
expect(mailboxMobileSection).toMatch(/\.mailbox-modal \.mailbox-header-actions \.btn-icon,[^}]*\.mailbox-view \.mailbox-header-actions \.btn-icon\s*\{[^}]*min-width:\s*36px;[^}]*display:\s*inline-flex;[^}]*\}/);
expect(mailboxMobileSection).toMatch(/\.mailbox-modal \.mailbox-header-actions \.btn,[^}]*\.mailbox-view \.mailbox-header-actions \.btn-icon\s*\{[^}]*min-height:\s*2\.25rem;[^}]*\}/);
expect(mailboxMobileSection).toMatch(/\.mailbox-modal \.mailbox-header-actions \.btn-icon,[^}]*\.mailbox-view \.mailbox-header-actions \.btn-icon\s*\{[^}]*min-width:\s*2\.25rem;[^}]*display:\s*inline-flex;[^}]*\}/);
expect(mailboxMobileSection).toMatch(/\.mailbox-modal \.mailbox-header-actions \.modal-close\s*\{[^}]*padding:\s*0;[^}]*border-radius:\s*var\(--radius-sm\);[^}]*\}/);
expect(mailboxMobileSection).toContain("overflow-x: auto;");
expect(mailboxMobileSection).toContain("-webkit-overflow-scrolling: touch;");
@@ -1078,7 +1114,7 @@ describe("MailboxModal", () => {
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-agent-select");
expect(mailboxMobileSection).toContain("max-width: 100%;");
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-agents");
expect(mailboxMobileSection).toContain("min-height: 200px;");
expect(mailboxMobileSection).toContain("min-height: 12.5rem;");
expect(mailboxMobileSection).toContain(".mailbox-modal .mailbox-empty");
expect(mailboxMobileSection).toContain("padding: var(--space-2xl) var(--space-md);");
});