Adds a shared cost-derivation utility and surfaces token/cost info in a new Cost tab on the task detail modal, plus an opt-in per-card cost badge on the board. - Extract token-cost calculation into a shared taskTokenCost helper (read-time costFor derivation) reused by the Summary tab, new Cost tab, and card badge - Add TaskDetailModal Cost tab (TaskCostTab.tsx/.css) showing cost breakdown for a task - Simplify TaskSummaryTab by delegating cost math to the shared helper - Add default-off project setting showCostBadgeOnCards (settings-schema.ts, types.ts) with a SettingsModal/AppearanceSection toggle - Add CostBadgeContext to thread the setting into TaskCard without prop drilling - Show an optional cost badge on TaskCard when the setting is enabled - Update i18n strings across en/es/fr/ko/zh-CN/zh-TW locales - Update docs (dashboard-guide.md, settings-reference.md) and add changeset fn-7820-cost-tab-and-card-badge.md Files changed: .changeset/fn-7820-cost-tab-and-card-badge.md | 7 ++ docs/dashboard-guide.md | 4 + docs/settings-reference.md | 1 + .../core/src/__tests__/settings-defaults.test.ts | 13 ++ packages/core/src/settings-schema.ts | 5 + packages/core/src/types.ts | 5 + packages/dashboard/app/App.tsx | 5 + .../dashboard/app/components/SettingsModal.tsx | 6 + packages/dashboard/app/components/TaskCard.css | 7 +- packages/dashboard/app/components/TaskCard.tsx | 27 +++- packages/dashboard/app/components/TaskCostTab.css | 51 ++++++++ packages/dashboard/app/components/TaskCostTab.tsx | 91 ++++++++++++++ .../dashboard/app/components/TaskDetailModal.tsx | 14 ++- .../dashboard/app/components/TaskSummaryTab.tsx | 123 +----------------- .../app/components/__tests__/TaskCard.test.tsx | 81 +++++++++++- .../app/components/__tests__/TaskCostTab.test.tsx | 55 ++++++++ .../TaskDetailModal.attachments-and-tabs.test.tsx | 11 +- .../settings/sections/AppearanceSection.tsx | 8 ++ .../sections/__tests__/AppearanceSection.test.tsx | 20 +++ .../settings-default-descriptions.test.tsx | 1 + .../dashboard/app/context/CostBadgeContext.tsx | 19 +++ packages/dashboard/app/hooks/useAppSettings.ts | 15 +++ .../app/utils/__tests__/taskTokenCost.test.ts | 62 +++++++++ packages/dashboard/app/utils/taskTokenCost.ts | 139 +++++++++++++++++++++ packages/i18n/locales/en/app.json | 29 ++++- packages/i18n/locales/es/app.json | 30 ++++- packages/i18n/locales/fr/app.json | 27 +++- packages/i18n/locales/ko/app.json | 30 ++++- packages/i18n/locales/zh-CN/app.json | 30 ++++- packages/i18n/locales/zh-TW/app.json | 30 ++++- 30 files changed, 789 insertions(+), 157 deletions(-) Fusion-Task-Id: FN-7820 Fusion-Task-Lineage: d33c5678-a68c-4b29-9db1-8ff0369dfd72 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
56 lines
2.3 KiB
TypeScript
56 lines
2.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { render, screen, within } from "@testing-library/react";
|
|
import type { TaskDetail, TaskTokenUsage } from "@fusion/core";
|
|
import { TaskCostTab } from "../TaskCostTab";
|
|
|
|
function usage(overrides: Partial<TaskTokenUsage> = {}): TaskTokenUsage {
|
|
return {
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
cachedTokens: 0,
|
|
cacheWriteTokens: 0,
|
|
totalTokens: 0,
|
|
firstUsedAt: "2026-01-01T00:00:00Z",
|
|
lastUsedAt: "2026-01-01T00:00:00Z",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function task(tokenUsage?: TaskTokenUsage): TaskDetail {
|
|
return { id: "FN-7820", title: "Cost", column: "in-progress", steps: [], dependencies: [], tokenUsage } as TaskDetail;
|
|
}
|
|
|
|
describe("TaskCostTab", () => {
|
|
it("renders per-model cost breakdown and task total", () => {
|
|
render(<TaskCostTab task={task(usage({
|
|
perModel: [
|
|
{ modelProvider: "openai", modelId: "gpt-5-mini", inputTokens: 1_000_000, outputTokens: 1_000_000, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 2_000_000 },
|
|
],
|
|
}))} />);
|
|
|
|
expect(screen.getByText("Model cost")).toBeInTheDocument();
|
|
const row = screen.getByTestId("task-cost-row");
|
|
expect(within(row).getByText("gpt-5-mini")).toBeInTheDocument();
|
|
expect(within(row).getAllByText("1,000,000")).toHaveLength(2);
|
|
expect(within(row).getByText("2,000,000")).toBeInTheDocument();
|
|
expect(within(row).getByText("$2.25")).toBeInTheDocument();
|
|
expect(screen.getByTestId("task-cost-total")).toHaveTextContent("Total: $2.25");
|
|
});
|
|
|
|
it("renders the unavailable sentinel for unpriceable models and total", () => {
|
|
render(<TaskCostTab task={task(usage({ modelProvider: "unknown", modelId: "no-price", inputTokens: 1, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 1 }))} />);
|
|
|
|
const row = screen.getByTestId("task-cost-row");
|
|
expect(within(row).getByText("—")).toBeInTheDocument();
|
|
expect(screen.getByTestId("task-cost-total")).toHaveTextContent("Total: —");
|
|
});
|
|
|
|
it("renders an explicit empty state when token usage is missing", () => {
|
|
render(<TaskCostTab task={task()} />);
|
|
|
|
expect(screen.getByTestId("task-cost-tab")).toBeInTheDocument();
|
|
expect(screen.getByText("No model token usage has been recorded for this task yet.")).toBeInTheDocument();
|
|
expect(screen.queryByTestId("task-cost-row")).toBeNull();
|
|
});
|
|
});
|