FN-8423: suppress duplicate task agent badges
Avoid redundant creator badges when a task is assigned to its creating agent. - Compare assigned and source agents by ID, with normalized names as a fallback - Preserve both badges for distinct agent identities and cover badge behavior with regression tests - Add a patch changeset for the TaskCard fix Files changed: .changeset/fn-8423-duplicate-agent-badge.md | 7 ++ packages/dashboard/app/components/TaskCard.tsx | 40 +++++++++++- .../app/components/__tests__/TaskCard.test.tsx | 74 +++++++++++++++++++++- 3 files changed, 119 insertions(+), 2 deletions(-) Fusion-Task-Id: FN-8423 Fusion-Task-Lineage: 2fb020c9-43e5-485e-80a9-1b723ee63ae8 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8423-duplicate-agent-badge.md
Normal file
7
.changeset/fn-8423-duplicate-agent-badge.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Show only one agent name badge when a task is assigned to its creator.
|
||||
category: fix
|
||||
dev: TaskCard suppresses redundant created-by pill when assigned and source agent identity match by ID-first rules (FN-8423).
|
||||
@@ -274,6 +274,35 @@ function isAgentCreatedTask(task: Task): boolean {
|
||||
return task.sourceType === "agent_heartbeat" || task.sourceType === "automation" || Boolean(getSourceAgentName(task));
|
||||
}
|
||||
|
||||
function getNormalizedAgentIdentity(value: string | null | undefined): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* FNXC:TaskCardAgentBadges 2026-07-20-00:00:
|
||||
* FN-8423 requires TaskCard to retain assigned ownership but suppress redundant
|
||||
* created-by provenance only for the same agent identity. IDs are authoritative:
|
||||
* distinct present IDs stay distinct even when their display names collide; names
|
||||
* are a case-insensitive fallback only when either identity has no usable ID.
|
||||
*/
|
||||
function isSameAgentIdentity(
|
||||
assignedAgentId: string | undefined,
|
||||
sourceAgentId: string | undefined,
|
||||
assignedAgentName: string | null | undefined,
|
||||
sourceAgentName: string | undefined,
|
||||
): boolean {
|
||||
const normalizedAssignedId = getNormalizedAgentIdentity(assignedAgentId);
|
||||
const normalizedSourceId = getNormalizedAgentIdentity(sourceAgentId);
|
||||
|
||||
if (normalizedAssignedId && normalizedSourceId) {
|
||||
return normalizedAssignedId === normalizedSourceId;
|
||||
}
|
||||
|
||||
const normalizedAssignedName = getNormalizedAgentIdentity(assignedAgentName)?.toLocaleLowerCase();
|
||||
const normalizedSourceName = getNormalizedAgentIdentity(sourceAgentName)?.toLocaleLowerCase();
|
||||
return Boolean(normalizedAssignedName && normalizedSourceName && normalizedAssignedName === normalizedSourceName);
|
||||
}
|
||||
|
||||
// ── Constants ───────────────────────────────────────────────────────────────
|
||||
|
||||
// Issue 1403: widened to ColumnId so `.has(task.column)` accepts custom column ids
|
||||
@@ -1405,6 +1434,15 @@ function TaskCardComponent({
|
||||
const resolvedAssignedAgentName = assignedAgentNameFromMap ?? assignedAgentNameFromCache ?? agentName;
|
||||
const assignedAgentBadgeLabel = resolvedAssignedAgentName ?? task.assignedAgentId ?? "";
|
||||
const isAgentNameLoading = Boolean(task.assignedAgentId && !resolvedAssignedAgentName);
|
||||
const shouldShowCreatedAgentBadge = isAgentCreated && !(
|
||||
task.assignedAgentId
|
||||
&& isSameAgentIdentity(
|
||||
task.assignedAgentId,
|
||||
task.sourceAgentId,
|
||||
resolvedAssignedAgentName,
|
||||
sourceAgentName,
|
||||
)
|
||||
);
|
||||
const taskProviders = useMemo(() => {
|
||||
const providers: string[] = [];
|
||||
if (task.modelProvider) providers.push(task.modelProvider);
|
||||
@@ -3742,7 +3780,7 @@ function TaskCardComponent({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isAgentCreated && (
|
||||
{shouldShowCreatedAgentBadge && (
|
||||
<div className="card-agent-badge-row" data-testid="card-agent-badge-row">
|
||||
{/**
|
||||
* FNXC:TaskCardLayout 2026-07-10-00:00:
|
||||
|
||||
@@ -5232,6 +5232,78 @@ describe("TaskCard", () => {
|
||||
expect(container.querySelector(".card-agent-badge-row")).toBeNull();
|
||||
});
|
||||
|
||||
it("FN-8423 keeps only the assigned badge for the same agent ID", () => {
|
||||
seedAgentsCache("fn-8423-same-id", [{ id: "agent-qa", name: "QA Engineer" }]);
|
||||
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
projectId="fn-8423-same-id"
|
||||
task={makeTask({
|
||||
column: "todo",
|
||||
assignedAgentId: "agent-qa",
|
||||
sourceType: "agent_heartbeat",
|
||||
sourceAgentId: "agent-qa",
|
||||
sourceMetadata: { agentName: "QA Engineer" },
|
||||
})}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
const assignedBadge = container.querySelector(".card-agent-badge");
|
||||
expect(assignedBadge).not.toBeNull();
|
||||
expect(assignedBadge).toHaveAttribute("title", "Assigned to QA Engineer");
|
||||
expect(container.querySelector(".card-agent-created-badge")).toBeNull();
|
||||
expect(container.querySelector(".card-agent-badge-row")).toBeNull();
|
||||
});
|
||||
|
||||
it("FN-8423 retains both badges for different IDs with the same display name", () => {
|
||||
seedAgentsCache("fn-8423-distinct-ids", [
|
||||
{ id: "agent-a", name: "QA Engineer" },
|
||||
{ id: "agent-b", name: "QA Engineer" },
|
||||
]);
|
||||
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
projectId="fn-8423-distinct-ids"
|
||||
task={makeTask({
|
||||
column: "todo",
|
||||
assignedAgentId: "agent-a",
|
||||
sourceType: "automation",
|
||||
sourceAgentId: "agent-b",
|
||||
})}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector(".card-agent-badge")).toHaveAttribute("title", "Assigned to QA Engineer");
|
||||
expect(container.querySelector(".card-agent-created-badge")).toHaveAttribute("title", "Created by agent: QA Engineer");
|
||||
expect(container.querySelector(".card-agent-badge-row")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("FN-8423 falls back to matching names only when source agent ID is unavailable", () => {
|
||||
seedAgentsCache("fn-8423-name-fallback", [{ id: "agent-qa", name: "QA Engineer" }]);
|
||||
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
projectId="fn-8423-name-fallback"
|
||||
task={makeTask({
|
||||
column: "todo",
|
||||
assignedAgentId: "agent-qa",
|
||||
sourceType: "automation",
|
||||
sourceMetadata: { agentName: " qa engineer " },
|
||||
})}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector(".card-agent-badge")).toHaveAttribute("title", "Assigned to QA Engineer");
|
||||
expect(container.querySelector(".card-agent-created-badge")).toBeNull();
|
||||
expect(container.querySelector(".card-agent-badge-row")).toBeNull();
|
||||
});
|
||||
|
||||
it("coexists with GitHub badge and timer metadata", () => {
|
||||
const { container } = render(
|
||||
<TaskCard
|
||||
@@ -6784,7 +6856,7 @@ describe("TaskCard workflow badges", () => {
|
||||
priority: "high",
|
||||
executionMode: "fast",
|
||||
sourceType: "automation",
|
||||
sourceMetadata: { agentName: "Task Robot" },
|
||||
sourceMetadata: { agentName: "Created Robot" },
|
||||
assignedAgentId: "agent-1",
|
||||
columnMovedAt: "2026-06-30T12:00:00.000Z",
|
||||
updatedAt: "2026-06-30T12:00:00.000Z",
|
||||
|
||||
Reference in New Issue
Block a user