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:
gsxdsm
2026-07-20 01:12:34 -07:00
parent 5af53bc94a
commit 02f8bffb2e
3 changed files with 119 additions and 2 deletions

View 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).

View File

@@ -274,6 +274,35 @@ function isAgentCreatedTask(task: Task): boolean {
return task.sourceType === "agent_heartbeat" || task.sourceType === "automation" || Boolean(getSourceAgentName(task)); 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 ─────────────────────────────────────────────────────────────── // ── Constants ───────────────────────────────────────────────────────────────
// Issue 1403: widened to ColumnId so `.has(task.column)` accepts custom column ids // 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 resolvedAssignedAgentName = assignedAgentNameFromMap ?? assignedAgentNameFromCache ?? agentName;
const assignedAgentBadgeLabel = resolvedAssignedAgentName ?? task.assignedAgentId ?? ""; const assignedAgentBadgeLabel = resolvedAssignedAgentName ?? task.assignedAgentId ?? "";
const isAgentNameLoading = Boolean(task.assignedAgentId && !resolvedAssignedAgentName); const isAgentNameLoading = Boolean(task.assignedAgentId && !resolvedAssignedAgentName);
const shouldShowCreatedAgentBadge = isAgentCreated && !(
task.assignedAgentId
&& isSameAgentIdentity(
task.assignedAgentId,
task.sourceAgentId,
resolvedAssignedAgentName,
sourceAgentName,
)
);
const taskProviders = useMemo(() => { const taskProviders = useMemo(() => {
const providers: string[] = []; const providers: string[] = [];
if (task.modelProvider) providers.push(task.modelProvider); 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"> <div className="card-agent-badge-row" data-testid="card-agent-badge-row">
{/** {/**
* FNXC:TaskCardLayout 2026-07-10-00:00: * FNXC:TaskCardLayout 2026-07-10-00:00:

View File

@@ -5232,6 +5232,78 @@ describe("TaskCard", () => {
expect(container.querySelector(".card-agent-badge-row")).toBeNull(); 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", () => { it("coexists with GitHub badge and timer metadata", () => {
const { container } = render( const { container } = render(
<TaskCard <TaskCard
@@ -6784,7 +6856,7 @@ describe("TaskCard workflow badges", () => {
priority: "high", priority: "high",
executionMode: "fast", executionMode: "fast",
sourceType: "automation", sourceType: "automation",
sourceMetadata: { agentName: "Task Robot" }, sourceMetadata: { agentName: "Created Robot" },
assignedAgentId: "agent-1", assignedAgentId: "agent-1",
columnMovedAt: "2026-06-30T12:00:00.000Z", columnMovedAt: "2026-06-30T12:00:00.000Z",
updatedAt: "2026-06-30T12:00:00.000Z", updatedAt: "2026-06-30T12:00:00.000Z",