From 40d125b9d1c02073e5e925b2b37f561f5cc148e5 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 18 Jul 2026 13:25:47 -0700 Subject: [PATCH] FN-8324: place cost badge below Promote Place enabled task cost badges directly below Promote while preserving their existing placement on other cards. - Render the shared cost badge in a Promote-specific bottom-right row - Preserve footer placement when Promote is unavailable - Add placement and visibility coverage plus a patch changeset Files changed: .changeset/fn-8324-card-cost-promote.md | 7 +++ packages/dashboard/app/components/TaskCard.css | 14 +++++ packages/dashboard/app/components/TaskCard.tsx | 41 +++++++----- packages/dashboard/app/components/__tests__/TaskCard.test.tsx | 72 ++++++++++++++++++++++ 4 files changed, 120 insertions(+), 14 deletions(-) Fusion-Task-Id: FN-8324 Fusion-Task-Lineage: 053da911-7f47-468c-a3b9-c0d85738bda8 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-8324-card-cost-promote.md | 7 ++ .../dashboard/app/components/TaskCard.css | 14 ++++ .../dashboard/app/components/TaskCard.tsx | 41 +++++++---- .../components/__tests__/TaskCard.test.tsx | 72 +++++++++++++++++++ 4 files changed, 120 insertions(+), 14 deletions(-) create mode 100644 .changeset/fn-8324-card-cost-promote.md diff --git a/.changeset/fn-8324-card-cost-promote.md b/.changeset/fn-8324-card-cost-promote.md new file mode 100644 index 0000000000..6099fcd70b --- /dev/null +++ b/.changeset/fn-8324-card-cost-promote.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Move the task-card cost badge below the Promote button in the bottom-right corner. +category: fix +dev: Cost chip (card-cost-indicator) now renders below .card-action-row on Promote-bearing cards; non-Promote card placement unchanged. diff --git a/packages/dashboard/app/components/TaskCard.css b/packages/dashboard/app/components/TaskCard.css index 4953003f59..2e7f93cf9e 100644 --- a/packages/dashboard/app/components/TaskCard.css +++ b/packages/dashboard/app/components/TaskCard.css @@ -889,6 +889,16 @@ at desktop and mobile breakpoints. row-gap: var(--space-xs); } +/* +FNXC:TaskCardCostBadge 2026-07-12-00:00: +On cards with Promote, the opt-in cost badge belongs directly below that action in the bottom-right corner. Keep its shared card-cost-indicator styling and only position its wrapper so cards without Promote retain their existing footer/meta layout. +*/ +.card-promote-cost-row { + display: flex; + justify-content: flex-end; + margin-top: var(--space-xs); +} + .card-provider-icons { display: inline-flex; align-items: center; @@ -1910,6 +1920,10 @@ The three-dot menu is the sole card move/action entry point. Keep this shared bu margin-left: auto; } + .card-promote-cost-row { + justify-content: flex-end; + } + /* Card: compact progress bar on narrow cards */ .card-progress { gap: var(--space-xs); diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index dbc7c2adf7..843b0667e7 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -2739,7 +2739,21 @@ function TaskCardComponent({ && Boolean(githubTrackedIssue); const footerHasLeadingContent = Boolean(filesChangedButton) || (isGitHubImportedTask && !showLinkedIssueChipForImport); - const footerRightHasContent = Boolean(cardCostLabel + const costBadgeBelowPromote = Boolean(onPromote && cardCostLabel); + const costBadgeChip = cardCostLabel ? ( + + {/* + FNXC:TaskCardCostBadge 2026-07-12-00:00: + The cost chip must show only the formatted amount because formatCost already includes the currency symbol; do not render a leading dollar-sign icon that duplicates the label. + */} + {cardCostLabel} + + ) : null; + const footerRightHasContent = Boolean((!costBadgeBelowPromote && cardCostLabel) || timeIndicator || showNearDuplicateChip || showUndoOfChip @@ -2849,19 +2863,7 @@ function TaskCardComponent({ FNXC:TaskCardTimingBadge 2026-06-13-17:20: The execution-time badge belongs in the bottom-right footer cluster and must match sibling footer badge sizing while preserving its existing label, title, aria text, and live-update data. */} - {cardCostLabel && ( - - {/* - FNXC:TaskCardCostBadge 2026-07-12-00:00: - The cost chip must show only the formatted amount because formatCost already includes the currency symbol; do not render a leading dollar-sign icon that duplicates the label. - */} - {cardCostLabel} - - )} + {!costBadgeBelowPromote && costBadgeChip} {timeIndicator && ( )} {shouldRenderActionRow && ( + <>
{showCreatePrQuickAction && (
+ {costBadgeBelowPromote && ( +
+ {/* + FNXC:TaskCardCostBadge 2026-07-12-00:00: + Promote-bearing cards must place the enabled cost badge directly below Promote in the bottom-right corner. Cards without Promote retain the footer/meta placement so other footer chips and card layouts do not move. + */} + {costBadgeChip} +
+ )} + )} {isAgentCreated && (
diff --git a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx index f33aab2e9e..6dc42fe9ed 100644 --- a/packages/dashboard/app/components/__tests__/TaskCard.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskCard.test.tsx @@ -5814,6 +5814,78 @@ describe("TaskCard", () => { expect(noUsage.container.querySelector(".card-cost-indicator")).toBeNull(); }); + it("places an enabled cost badge below Promote without leaving it in the footer cluster", () => { + const pricedTask = makeTask({ + id: "FN-8324", + column: "todo", + tokenUsage: { + inputTokens: 1_000_000, + outputTokens: 0, + cachedTokens: 0, + cacheWriteTokens: 0, + totalTokens: 1_000_000, + firstUsedAt: "2026-01-01T00:00:00Z", + lastUsedAt: "2026-01-01T00:00:00Z", + modelProvider: "openai", + modelId: "gpt-5-mini", + }, + } as Partial); + + const { container } = render( + + + , + ); + + const promoteButton = screen.getByTestId("card-promote-FN-8324"); + const costBadge = container.querySelector(".card-cost-indicator") as HTMLElement | null; + const costRow = container.querySelector(".card-promote-cost-row"); + expect(costBadge).not.toBeNull(); + expect(costBadge?.closest(".card-promote-cost-row")).toBe(costRow); + expect(costBadge?.closest(".card-footer-row-right")).toBeNull(); + expect(promoteButton.compareDocumentPosition(costBadge!) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); + + const css = loadAllAppCss(); + expect(css).toMatch(/\.card-promote-cost-row\s*\{[^}]*justify-content:\s*flex-end;[^}]*margin-top:\s*var\(--space-xs\);[^}]*\}/); + expect(css).toMatch(/@media[^{}]*\(max-width:\s*768px\)[^{]*\{[\s\S]*?\.card-promote-cost-row\s*\{[^}]*justify-content:\s*flex-end;[^}]*\}/); + }); + + it("keeps the cost badge absent below Promote when disabled or without token usage", () => { + const onPromote = vi.fn().mockResolvedValue(undefined); + const pricedTask = makeTask({ + id: "FN-8324-disabled", + column: "todo", + tokenUsage: { + inputTokens: 1_000_000, + outputTokens: 0, + cachedTokens: 0, + cacheWriteTokens: 0, + totalTokens: 1_000_000, + firstUsedAt: "2026-01-01T00:00:00Z", + lastUsedAt: "2026-01-01T00:00:00Z", + modelProvider: "openai", + modelId: "gpt-5-mini", + }, + } as Partial); + const disabled = render(); + expect(disabled.container.querySelector(".card-cost-indicator")).toBeNull(); + expect(disabled.container.querySelector(".card-promote-cost-row")).toBeNull(); + disabled.unmount(); + + const noUsage = render( + + + , + ); + expect(noUsage.container.querySelector(".card-cost-indicator")).toBeNull(); + expect(noUsage.container.querySelector(".card-promote-cost-row")).toBeNull(); + }); + it("places a todo cost badge inside the meta row when the footer has no leading content", () => { const { container } = render(