diff --git a/.changeset/taskcard-time-indicator-renamed-lanes.md b/.changeset/taskcard-time-indicator-renamed-lanes.md new file mode 100644 index 0000000000..858d8a728c --- /dev/null +++ b/.changeset/taskcard-time-indicator-renamed-lanes.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Cards in renamed in-progress, review or completion lanes now show their elapsed-time indicator. +category: fix +dev: The `TIME_INDICATOR_COLUMNS` legacy id set gated both the indicator memo and the chip layout; it is now a role question with that set kept as the no-flags fallback. diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index e96da7faf8..9e1c638023 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -1063,6 +1063,26 @@ function TaskCardComponent({ const isCompleteColumn = isCompleteColumnRole(taskColumnFlags, task.column); const isArchivedColumn = isArchivedColumnRole(taskColumnFlags, task.column); + /* + FNXC:WorkflowResolvedColumns 2026-07-31-03:15: + THE TIME-INDICATOR GATE IS A ROLE QUESTION, not membership of a hardcoded id set. + + `TIME_INDICATOR_COLUMNS` is `{in-progress, in-review, done}`, so a card in a renamed WIP, review or + completion lane was rejected before its traits were consulted and rendered no elapsed time at all. + #2996 fixed the SUBSCRIPTION for these cards — the memo that decides whether to join the shared + ticker kept a pre-load answer — which made them eligible and still not visible, because this gate + rejects them first. Both halves are needed; that PR's claim covered only one. + + The census could not point here: it counts COMPARISONS against legacy ids, and this is a Set + literal, which is a DEFINITION. Same blind spot that hid `BLOCKER_ESCALATION_COLUMNS`. + + DELIBERATE-LITERAL — the legacy set stays as the no-flags fallback, so a card whose traits have not + resolved (first paint, or a lane its workflow no longer declares) behaves exactly as before. + */ + const showsTimeIndicator = taskColumnFlags + ? isWipColumn || isReviewColumn || isCompleteColumn + : TIME_INDICATOR_COLUMNS.has(task.column); + const [isSaving, setIsSaving] = useState(false); const [showSteps, setShowSteps] = useState( isWipColumn || @@ -1754,7 +1774,7 @@ function TaskCardComponent({ const timeIndicatorNowMs = useLiveTimeTicker(wantsLiveTimeIndicator); const timeIndicator = useMemo(() => { - if (!TIME_INDICATOR_COLUMNS.has(task.column)) { + if (!showsTimeIndicator) { return null; } @@ -3129,7 +3149,7 @@ function TaskCardComponent({ return null; })(); - const chipFarRight = TIME_INDICATOR_COLUMNS.has(task.column) + const chipFarRight = showsTimeIndicator && filesChangedButton == null && showTrackingIndicator && Boolean(githubTrackedIssue); diff --git a/packages/dashboard/app/components/__tests__/TaskCard.time-indicator-renamed-lanes.test.tsx b/packages/dashboard/app/components/__tests__/TaskCard.time-indicator-renamed-lanes.test.tsx new file mode 100644 index 0000000000..0a80d47432 --- /dev/null +++ b/packages/dashboard/app/components/__tests__/TaskCard.time-indicator-renamed-lanes.test.tsx @@ -0,0 +1,90 @@ +/* +FNXC:WorkflowResolvedColumns 2026-07-31-03:10: +THE TIME INDICATOR IS GATED ON A HARDCODED LEGACY LANE SET, so it never renders on a renamed board. + +`TIME_INDICATOR_COLUMNS` is `{in-progress, in-review, done}`. Both the `timeIndicator` memo and the +`chipFarRight` layout test `task.column` against it directly, so a card in a renamed WIP, review or +completion lane returns `null` no matter what its resolved traits say. + +WHY THIS IS A SEPARATE DEFECT FROM #2996, and a correction to that PR's claim: #2996 fixed the +SUBSCRIPTION — `wantsLiveTimeIndicator` kept a pre-load answer and the card never joined the shared +ticker. That was real, and it was not sufficient. The card now subscribes and still renders nothing, +because this gate rejects it first. I described that PR as making renamed-lane cards "show their live +elapsed-time indicator"; it made them eligible to. + +WHY NO CHECK SAW IT. The census counts COMPARISONS against legacy ids. This is a Set literal — a +DEFINITION — so nothing in the backlog ever pointed here, the same blind spot that hid +`BLOCKER_ESCALATION_COLUMNS` until it was found by hand. +*/ + +import { describe, it, expect, vi } from "vitest"; +import { render } from "@testing-library/react"; +import type { Task } from "@fusion/core"; +import { TaskCard } from "../TaskCard"; + +vi.mock("../../hooks/useLiveTimeTicker", () => ({ + useLiveTimeTicker: () => Date.parse("2026-06-01T01:00:00.000Z"), +})); + +const noop = () => {}; + +function runningTaskIn(column: string): Task { + return { + id: "KB-1", + title: "a running card", + description: "t", + column, + createdAt: "2026-06-01T00:00:00.000Z", + updatedAt: "2026-06-01T00:00:00.000Z", + executionStartedAt: "2026-06-01T00:00:00.000Z", + cumulativeActiveMs: 120_000, + timedExecutionMs: 120_000, + steps: [], + } as unknown as Task; +} + +/** The indicator renders an elapsed duration; any of these means it is present. */ +const hasDuration = (root: HTMLElement) => /\d+\s*(s|m|h)\b|\d+:\d\d/.test(root.textContent ?? ""); + +describe("the card time indicator under a renamed board vocabulary", () => { + /* Control: the legacy vocabulary renders it, with no flags at all. */ + it("default vocabulary: a card in `in-progress` shows an elapsed time", () => { + const { container } = render( + , + ); + + expect(hasDuration(container as unknown as HTMLElement)).toBe(true); + }); + + /* The defect: the renamed WIP lane is not in the hardcoded set, so nothing renders. */ + it("renamed vocabulary: a card whose traits say WIP shows an elapsed time", () => { + const { container } = render( + , + ); + + expect(hasDuration(container as unknown as HTMLElement)).toBe(true); + }); + + /* + The paired negative: resolving traits must not put a live timer on every lane. A card in the + renamed INTAKE lane has not started, so it must stay out — otherwise the fix trades a missing + indicator for a running clock on work that has not begun. + */ + it("renamed vocabulary: a card in the intake lane shows no elapsed time", () => { + const { container } = render( + , + ); + + expect(hasDuration(container as unknown as HTMLElement)).toBe(false); + }); +});