feat(FN-2685): remove TaskCard token usage badge
- Remove token usage badge rendering and compact token formatting from TaskCard - Drop token usage fields from TaskCard memo equality checks now that the badge is gone - Delete obsolete TaskCard token badge CSS rules and icon import - Simplify TaskCard tests by removing token badge assertions and related fixtures
This commit is contained in:
@@ -478,21 +478,6 @@
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-token-usage {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--space-xs);
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
|
|
||||||
line-height: 1;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-token-usage-value {
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-time-indicator {
|
.card-time-indicator {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1004,10 +989,6 @@
|
|||||||
gap: var(--space-xs);
|
gap: var(--space-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-token-usage {
|
|
||||||
font-size: calc(var(--space-sm) + var(--space-xs) * 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-time-indicator {
|
.card-time-indicator {
|
||||||
padding: var(--space-xs) var(--space-sm);
|
padding: var(--space-xs) var(--space-sm);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import "./TaskCard.css";
|
import "./TaskCard.css";
|
||||||
import { memo, useCallback, useState, useRef, useEffect, useMemo } from "react";
|
import { memo, useCallback, useState, useRef, useEffect, useMemo } from "react";
|
||||||
import { Link, Clock, Layers, Pencil, ChevronDown, Folder, Target, Bot, Trash2, Zap } from "lucide-react";
|
import { Link, Clock, Layers, Pencil, ChevronDown, Folder, Target, Bot, Trash2 } from "lucide-react";
|
||||||
import type { Task, TaskDetail, Column, PrInfo, IssueInfo, TaskPriority } from "@fusion/core";
|
import type { Task, TaskDetail, Column, PrInfo, IssueInfo, TaskPriority } from "@fusion/core";
|
||||||
import { COLUMN_LABELS, DEFAULT_TASK_PRIORITY, TASK_PRIORITIES, VALID_TRANSITIONS, getErrorMessage } from "@fusion/core";
|
import { COLUMN_LABELS, DEFAULT_TASK_PRIORITY, TASK_PRIORITIES, VALID_TRANSITIONS, getErrorMessage } from "@fusion/core";
|
||||||
import { fetchTaskDetail, uploadAttachment, fetchMission, fetchAgent } from "../api";
|
import { fetchTaskDetail, uploadAttachment, fetchMission, fetchAgent } from "../api";
|
||||||
@@ -165,23 +165,6 @@ function formatElapsedDuration(elapsedMs: number): string {
|
|||||||
return `${elapsedDays}d`;
|
return `${elapsedDays}d`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCompactTokenCount(value: number): string {
|
|
||||||
if (!Number.isFinite(value) || value < 0) {
|
|
||||||
return "0";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value < 1_000) {
|
|
||||||
return String(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const trimTrailingDecimal = (formatted: string) => formatted.replace(/\.0$/, "");
|
|
||||||
|
|
||||||
if (value < 1_000_000) {
|
|
||||||
return `${trimTrailingDecimal((value / 1_000).toFixed(1))}k`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${trimTrailingDecimal((value / 1_000_000).toFixed(1))}M`;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TaskCardProps {
|
interface TaskCardProps {
|
||||||
task: Task;
|
task: Task;
|
||||||
@@ -371,8 +354,6 @@ function areTaskCardPropsEqual(previous: TaskCardProps, next: TaskCardProps): bo
|
|||||||
previousTask.missionId === nextTask.missionId &&
|
previousTask.missionId === nextTask.missionId &&
|
||||||
previousTask.assignedAgentId === nextTask.assignedAgentId &&
|
previousTask.assignedAgentId === nextTask.assignedAgentId &&
|
||||||
previousTask.mergeRetries === nextTask.mergeRetries &&
|
previousTask.mergeRetries === nextTask.mergeRetries &&
|
||||||
previousTask.tokenUsage?.totalTokens === nextTask.tokenUsage?.totalTokens &&
|
|
||||||
previousTask.tokenUsage?.lastUsedAt === nextTask.tokenUsage?.lastUsedAt &&
|
|
||||||
areAttachmentsEqual(previousTask.attachments, nextTask.attachments) &&
|
areAttachmentsEqual(previousTask.attachments, nextTask.attachments) &&
|
||||||
areCommentsEqual(previousTask.comments, nextTask.comments) &&
|
areCommentsEqual(previousTask.comments, nextTask.comments) &&
|
||||||
areTaskDependenciesEqual(previousTask.dependencies, nextTask.dependencies) &&
|
areTaskDependenciesEqual(previousTask.dependencies, nextTask.dependencies) &&
|
||||||
@@ -1131,18 +1112,6 @@ function TaskCardComponent({
|
|||||||
return null;
|
return null;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const tokenUsageIndicator = task.tokenUsage ? (
|
|
||||||
<span
|
|
||||||
className="card-token-usage"
|
|
||||||
title={`${task.tokenUsage.totalTokens.toLocaleString()} tokens`}
|
|
||||||
aria-label={`Token usage ${formatCompactTokenCount(task.tokenUsage.totalTokens)} tokens`}
|
|
||||||
>
|
|
||||||
<Zap size={12} />
|
|
||||||
<span className="card-token-usage-value">{formatCompactTokenCount(task.tokenUsage.totalTokens)}</span>
|
|
||||||
<span>tokens</span>
|
|
||||||
</span>
|
|
||||||
) : null;
|
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -1415,10 +1384,9 @@ function TaskCardComponent({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
{(filesChangedButton || timeIndicator || tokenUsageIndicator) && (
|
{(filesChangedButton || timeIndicator) && (
|
||||||
<div className="card-footer-row">
|
<div className="card-footer-row">
|
||||||
{filesChangedButton}
|
{filesChangedButton}
|
||||||
{tokenUsageIndicator}
|
|
||||||
{timeIndicator && (
|
{timeIndicator && (
|
||||||
<span
|
<span
|
||||||
className="card-time-indicator"
|
className="card-time-indicator"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { afterEach, describe, it, expect, vi } from "vitest";
|
import { afterEach, describe, it, expect, vi } from "vitest";
|
||||||
import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
|
import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
|
||||||
import { TaskCard, __test_areTaskCardPropsEqual } from "../TaskCard";
|
import { TaskCard } from "../TaskCard";
|
||||||
import type { Task } from "@fusion/core";
|
import type { Task } from "@fusion/core";
|
||||||
|
|
||||||
// Mock lucide-react to avoid SVG rendering issues in test env
|
// Mock lucide-react to avoid SVG rendering issues in test env
|
||||||
@@ -16,7 +16,6 @@ vi.mock("lucide-react", () => ({
|
|||||||
Target: () => null,
|
Target: () => null,
|
||||||
Bot: () => null,
|
Bot: () => null,
|
||||||
Trash2: () => null,
|
Trash2: () => null,
|
||||||
Zap: () => null,
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../ProviderIcon", () => ({
|
vi.mock("../ProviderIcon", () => ({
|
||||||
@@ -48,15 +47,6 @@ function makeTask(overrides: Partial<Task> = {}): Task {
|
|||||||
|
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
const tokenUsageFixture = {
|
|
||||||
inputTokens: 50_000,
|
|
||||||
outputTokens: 30_000,
|
|
||||||
cachedTokens: 10_000,
|
|
||||||
totalTokens: 90_000,
|
|
||||||
firstUsedAt: "2026-04-26T10:00:00.000Z",
|
|
||||||
lastUsedAt: "2026-04-26T10:30:00.000Z",
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
});
|
});
|
||||||
@@ -67,33 +57,6 @@ describe("TaskCard", () => {
|
|||||||
expect(screen.getByText("FN-001")).toBeDefined();
|
expect(screen.getByText("FN-001")).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders token usage indicator when task has token usage", () => {
|
|
||||||
const { container } = render(
|
|
||||||
<TaskCard
|
|
||||||
task={makeTask({ tokenUsage: { ...tokenUsageFixture } })}
|
|
||||||
onOpenDetail={noop}
|
|
||||||
addToast={noop}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const tokenUsage = container.querySelector(".card-token-usage");
|
|
||||||
expect(tokenUsage).not.toBeNull();
|
|
||||||
expect(tokenUsage?.textContent).toContain("90k");
|
|
||||||
expect(tokenUsage?.textContent).toContain("tokens");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("does not render token usage indicator when tokenUsage is undefined", () => {
|
|
||||||
const { container } = render(
|
|
||||||
<TaskCard
|
|
||||||
task={makeTask({ tokenUsage: undefined })}
|
|
||||||
onOpenDetail={noop}
|
|
||||||
addToast={noop}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(container.querySelector(".card-token-usage")).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders the status badge when task.status is set", () => {
|
it("renders the status badge when task.status is set", () => {
|
||||||
render(
|
render(
|
||||||
<TaskCard
|
<TaskCard
|
||||||
@@ -740,32 +703,6 @@ describe("TaskCard", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("TaskCard memo comparator", () => {
|
|
||||||
it("detects token usage changes", () => {
|
|
||||||
type ComparatorProps = Parameters<typeof __test_areTaskCardPropsEqual>[0];
|
|
||||||
|
|
||||||
const baseTask = makeTask({ tokenUsage: { ...tokenUsageFixture } });
|
|
||||||
const baseProps: ComparatorProps = {
|
|
||||||
task: baseTask,
|
|
||||||
onOpenDetail: noop,
|
|
||||||
addToast: noop,
|
|
||||||
};
|
|
||||||
|
|
||||||
const totalTokenChange: ComparatorProps = {
|
|
||||||
...baseProps,
|
|
||||||
task: makeTask({ tokenUsage: { ...tokenUsageFixture, totalTokens: 95_000 } }),
|
|
||||||
};
|
|
||||||
|
|
||||||
const lastUsedAtChange: ComparatorProps = {
|
|
||||||
...baseProps,
|
|
||||||
task: makeTask({ tokenUsage: { ...tokenUsageFixture, lastUsedAt: "2026-04-26T10:35:00.000Z" } }),
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(__test_areTaskCardPropsEqual(baseProps, totalTokenChange)).toBe(false);
|
|
||||||
expect(__test_areTaskCardPropsEqual(baseProps, lastUsedAtChange)).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("TaskCard provider icons on agent row", () => {
|
describe("TaskCard provider icons on agent row", () => {
|
||||||
it("renders provider icons when task has model overrides", () => {
|
it("renders provider icons when task has model overrides", () => {
|
||||||
render(
|
render(
|
||||||
|
|||||||
Reference in New Issue
Block a user