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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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);");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user