FN-8636: hide unavailable task card cost badges
Hide dash-only cost badges from board task cards when pricing is unavailable. - Suppress unavailable cost labels and their empty layout shells. - Cover priced and unavailable badges with and without Promote across desktop and mobile widths. - Add a patch changeset for the board-card fix. Files changed: .changeset/fn-8636-card-cost-badge-dash.md | 7 ++ packages/dashboard/app/components/TaskCard.tsx | 8 +- .../__tests__/TaskCard.cost-badge.test.tsx | 93 ++++++++++++++++------ .../app/components/__tests__/TaskCard.test.tsx | 14 ++-- 4 files changed, 84 insertions(+), 38 deletions(-) Fusion-Task-Id: FN-8636 Fusion-Task-Lineage: 31fbf82a-7a67-4629-bf82-48faf3c3a9d7 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-8636-card-cost-badge-dash.md
Normal file
7
.changeset/fn-8636-card-cost-badge-dash.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Hide unavailable cost badges on board task cards.
|
||||
category: fix
|
||||
dev: Board cards now omit unavailable costs while detail and analytics surfaces retain the guess-free sentinel.
|
||||
@@ -1158,13 +1158,15 @@ function TaskCardComponent({
|
||||
const retryWarningThreshold = useRetryWarning();
|
||||
const costBadge = useCostBadge();
|
||||
/*
|
||||
FNXC:TaskCardCostBadge 2026-07-11-12:20:
|
||||
The optional spend chip sits beside execution time but stays fully absent unless the default-off setting is enabled and the task has positive token usage. Use the shared read-time taskTokenCost helper so unpriced models show the guess-free “—” sentinel instead of a fabricated $0.
|
||||
FNXC:TaskCardCostBadge 2026-07-31-23:39:
|
||||
Board cards must omit the optional spend chip entirely when the derived cost is unavailable, avoiding a dash-only badge or empty Promote row. Detail and analytics surfaces retain formatCost's guess-free “—” sentinel rather than fabricating a $0 value.
|
||||
*/
|
||||
const cardCost = costBadge.enabled && hasTaskCost(task as TaskDetail)
|
||||
? taskTotalCost(task as TaskDetail, costBadge.pricingOverrides)
|
||||
: null;
|
||||
const cardCostLabel = cardCost ? formatCost(cardCost.usd, cardCost.unavailable) : null;
|
||||
const cardCostLabel = cardCost && !cardCost.unavailable && cardCost.usd !== null
|
||||
? formatCost(cardCost.usd, false)
|
||||
: null;
|
||||
|
||||
// Touch gesture detection refs
|
||||
const touchStartPosRef = useRef<{ x: number; y: number; time: number } | null>(null);
|
||||
|
||||
@@ -108,33 +108,74 @@ describe("TaskCard cost badge", () => {
|
||||
expect(badges[0]?.closest(".card-footer-row-right")).not.toBeNull();
|
||||
});
|
||||
|
||||
it("keeps unpriced usage guess-free and renders once below Promote", () => {
|
||||
const { container } = render(
|
||||
<CostBadgeProvider value={{ enabled: true }}>
|
||||
<TaskCard
|
||||
task={taskWithUsage({
|
||||
tokenUsage: {
|
||||
...taskWithUsage().tokenUsage!,
|
||||
modelProvider: "unknown",
|
||||
modelId: "no-price",
|
||||
perModel: [
|
||||
{ modelProvider: "openai", modelId: "gpt-5-mini", inputTokens: 1_000_000, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 1_000_000 },
|
||||
{ modelProvider: "unknown", modelId: "no-price", inputTokens: 1, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 1 },
|
||||
],
|
||||
},
|
||||
})}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
onPromote={vi.fn().mockResolvedValue(undefined)}
|
||||
/>
|
||||
</CostBadgeProvider>,
|
||||
);
|
||||
it.each([1280, 390])("omits unavailable cost chips and their shells at %ipx with or without Promote", (width) => {
|
||||
const originalWidth = window.innerWidth;
|
||||
Object.defineProperty(window, "innerWidth", { configurable: true, value: width });
|
||||
|
||||
const badges = container.querySelectorAll(".card-cost-indicator");
|
||||
expect(badges).toHaveLength(1);
|
||||
expect(badges[0]?.textContent).toBe("—");
|
||||
expect(badges[0]?.closest(".card-promote-cost-row")).not.toBeNull();
|
||||
expect(badges[0]?.closest(".card-footer-row-right")).toBeNull();
|
||||
try {
|
||||
const unavailableWithoutPromote = render(
|
||||
<CostBadgeProvider value={{ enabled: true }}>
|
||||
<TaskCard
|
||||
task={taskWithUsage({
|
||||
tokenUsage: { ...taskWithUsage().tokenUsage!, modelProvider: "unknown", modelId: "no-price" },
|
||||
})}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
/>
|
||||
</CostBadgeProvider>,
|
||||
);
|
||||
expect(unavailableWithoutPromote.container.querySelector(".card-cost-indicator")).toBeNull();
|
||||
expect(unavailableWithoutPromote.container.querySelector(".card-cost-indicator[aria-label]")).toBeNull();
|
||||
expect(unavailableWithoutPromote.container.querySelector(".card-promote-cost-row")).toBeNull();
|
||||
unavailableWithoutPromote.unmount();
|
||||
|
||||
const unavailableWithPromote = render(
|
||||
<CostBadgeProvider value={{ enabled: true }}>
|
||||
<TaskCard
|
||||
task={taskWithUsage({
|
||||
tokenUsage: {
|
||||
...taskWithUsage().tokenUsage!,
|
||||
modelProvider: "unknown",
|
||||
modelId: "no-price",
|
||||
perModel: [
|
||||
{ modelProvider: "openai", modelId: "gpt-5-mini", inputTokens: 1_000_000, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 1_000_000 },
|
||||
{ modelProvider: "unknown", modelId: "no-price", inputTokens: 1, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 1 },
|
||||
],
|
||||
},
|
||||
})}
|
||||
onOpenDetail={noop}
|
||||
addToast={noop}
|
||||
onPromote={vi.fn().mockResolvedValue(undefined)}
|
||||
/>
|
||||
</CostBadgeProvider>,
|
||||
);
|
||||
expect(unavailableWithPromote.container.querySelector(".card-cost-indicator")).toBeNull();
|
||||
expect(unavailableWithPromote.container.querySelector(".card-cost-indicator[aria-label]")).toBeNull();
|
||||
expect(unavailableWithPromote.container.querySelector(".card-promote-cost-row")).toBeNull();
|
||||
unavailableWithPromote.unmount();
|
||||
|
||||
const pricedWithoutPromote = render(
|
||||
<CostBadgeProvider value={{ enabled: true }}>
|
||||
<TaskCard task={taskWithUsage()} onOpenDetail={noop} addToast={noop} />
|
||||
</CostBadgeProvider>,
|
||||
);
|
||||
const footerBadge = pricedWithoutPromote.container.querySelectorAll(".card-cost-indicator");
|
||||
expect(footerBadge).toHaveLength(1);
|
||||
expect(footerBadge[0]?.textContent).toContain("$");
|
||||
pricedWithoutPromote.unmount();
|
||||
|
||||
const pricedWithPromote = render(
|
||||
<CostBadgeProvider value={{ enabled: true }}>
|
||||
<TaskCard task={taskWithUsage()} onOpenDetail={noop} addToast={noop} onPromote={vi.fn().mockResolvedValue(undefined)} />
|
||||
</CostBadgeProvider>,
|
||||
);
|
||||
const promoteBadge = pricedWithPromote.container.querySelectorAll(".card-cost-indicator");
|
||||
expect(promoteBadge).toHaveLength(1);
|
||||
expect(promoteBadge[0]?.textContent).toContain("$");
|
||||
expect(promoteBadge[0]?.closest(".card-promote-cost-row")).not.toBeNull();
|
||||
} finally {
|
||||
Object.defineProperty(window, "innerWidth", { configurable: true, value: originalWidth });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the shared card cost chip visible at the mobile breakpoint", () => {
|
||||
|
||||
@@ -6617,7 +6617,7 @@ describe("TaskCard", () => {
|
||||
expect(container.querySelector(".card-footer-row")).toBeNull();
|
||||
});
|
||||
|
||||
it("places the unavailable cost sentinel inside todo meta without adding an icon", () => {
|
||||
it("omits an unavailable cost chip and its footer shell", () => {
|
||||
const { container } = render(
|
||||
<CostBadgeProvider value={{ enabled: true }}>
|
||||
<TaskCard
|
||||
@@ -6642,14 +6642,10 @@ describe("TaskCard", () => {
|
||||
</CostBadgeProvider>,
|
||||
);
|
||||
|
||||
const costBadge = container.querySelector(".card-cost-indicator") as HTMLElement | null;
|
||||
expect(costBadge).not.toBeNull();
|
||||
expect(costBadge?.textContent).toContain("—");
|
||||
expect(costBadge?.querySelector("svg")).toBeNull();
|
||||
expect(costBadge?.getAttribute("aria-label")).toBe("Estimated cost —");
|
||||
expect(costBadge?.getAttribute("title")).toBe("Estimated cost —");
|
||||
expect(costBadge?.closest(".card-meta")).toBe(container.querySelector(".card-meta"));
|
||||
expect(costBadge?.closest(".card-footer-row")).toBeNull();
|
||||
expect(container.querySelector(".card-cost-indicator")).toBeNull();
|
||||
expect(container.querySelector(".card-cost-indicator[aria-label]")).toBeNull();
|
||||
expect(container.querySelector(".card-promote-cost-row")).toBeNull();
|
||||
expect(container.querySelector(".card-footer-row-right")).toBeNull();
|
||||
expect(container.querySelector(".card-footer-row")).toBeNull();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user