Phase B — Column.tsx 2→0 and taskActivity.ts 2→0 (U11's cluster to zero) (#2636)
**Claimed:** `Column.tsx`, `taskActivity.ts` — both to zero.
| file | before | after |
|---|---:|---:|
| `packages/dashboard/app/components/Column.tsx` | **2** | **0** |
| `packages/dashboard/app/utils/taskActivity.ts` | **2** | **0** |
## `Column.tsx` — two different fixes, because the two sites are
different problems
**The preserve-progress prompt** routed through
`isPreImplementationColumnRole`. This is the *same* question that helper
was written for — ListView asks it about a move target, Column asks it
about itself — and the degraded id sets are identical (`{todo,
triage}`), so the consolidation is exact.
I verified the sets matched **before** consolidating, because the
sibling case is not interchangeable: `isPreExecutionHoldColumn` in
`TaskContextMenu` drives the Plan affordance and its degraded set is
`{triage}` alone. Routing *that* through this helper added `plan` to
flagless `todo` cards, caught by an existing test. **Same shape,
identical trait path, non-interchangeable fallbacks.**
**The legacy-board arm** (`workflowMode || column === "triage"`) —
deleted, on the third attempt.
I deleted it twice before and reverted both times because four Column
tests render without `workflowMode`. That was the delete-only rule
working, but **my conclusion from it was wrong**: a behaviour change
means the branch was not dead *for those callers*, and the callers are
**fixtures, not production**. Board is Column's only consumer and passes
`workflowMode` at all three render sites. Defending an unreachable arm
so four tests keep passing preserves the tests, not the behaviour.
Two notes for anyone converting the remaining dashboard files:
- I did **not** default `workflowMode` to `true`, which was the tempting
one-liner. `isArchived`, `isHoldColumn` and `isWipProcessingColumn` all
switch on that same flag, so a global default would silently reinterpret
every other fixture in an 85-test file.
- **"Four tests break" was itself an underestimate.** Two more FN-770
fixtures surfaced only after the first two were fixed, because they
render their own explicit `column="triage"` block instead of using
`defaultProps`. The blast radius only became accurate by fixing it in
waves.
## `taskActivity.ts` — composed, not copied
The degraded arm now composes `utils/columnRoles`' predicates instead of
naming ids. **No local copy** — which is the failure mode #2625 hit from
the other direction.
Equivalent *by construction*:
| lane | composition | resolves to |
|---|---|---|
| intake | `isIntakeColumnRole(undefined, col)` | `triage` |
| hold | `isPreImplementationColumnRole(...)` **and not** intake |
`todo` |
reproducing `col === "triage" || (col === "todo" && isReplanning)`
exactly, since the shared pre-implementation set is `{todo, triage}` and
the shared intake id is `triage`.
Deliberately phrased as *"pre-implementation and not intake"* rather
than a second id list: if either shared set changes, this composition
follows it instead of silently disagreeing with the file next door. That
disagreement is precisely what bit the `TaskContextMenu` consolidation
above.
**I previously reported this site as blocked on `TaskCard.tsx` (U12's)**
— on the theory that the arm could only die once every caller supplied
resolved flags. Wrong framing: the arm doesn't need to become
*unreachable*, it needs to stop *naming ids*. Composing the shared
predicates does that without touching any caller.
## Verification
**1139 of 1141** green across `app/utils`, `Column` and `TaskCard`
suites. The two `TaskCard` failures are **pre-existing** — verified by
stashing this change and re-running, where they fail identically.
Dashboard app typecheck and lint clean.
Takes U11's cluster to zero except `TaskContextMenu.tsx`, whose
remaining site is covered in **#2626** and whose second site is a
documented non-consolidation.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import { QuickEntryBox } from "./QuickEntryBox";
|
||||
import { PluginSlot } from "./PluginSlot";
|
||||
import { groupByWorktree } from "../utils/worktreeGrouping";
|
||||
import { isTaskAgentActive } from "../utils/taskActivity";
|
||||
import { isPreImplementationColumnRole } from "../utils/columnRoles";
|
||||
import { isTaskStuck } from "../utils/taskStuck";
|
||||
import type { ToastType } from "../hooks/useToast";
|
||||
import type { TaskContextMenuColumnMetadata } from "./TaskContextMenu";
|
||||
@@ -441,11 +442,19 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktree
|
||||
where the card and the destination differ. Ids remain the fallback for the
|
||||
no-metadata window.
|
||||
*/
|
||||
const shouldPrompt = hasStepProgress && (
|
||||
columnFlags
|
||||
? Boolean(columnFlags.intake || columnFlags.hold)
|
||||
: column === "todo" || column === "triage"
|
||||
);
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-19:20 (Phase B — consolidated, semantics verified):
|
||||
Routed through `isPreImplementationColumnRole`. This is the SAME preserve-progress prompt that
|
||||
helper was written for — ListView asks it about a move target, this component asks it about
|
||||
itself — and the degraded id sets are identical (`{todo, triage}`), so the consolidation is
|
||||
exact rather than approximately right.
|
||||
|
||||
Verified before consolidating, because the sibling case in TaskContextMenu is NOT
|
||||
interchangeable: `isPreExecutionHoldColumn` drives the Plan affordance and its degraded set is
|
||||
`{triage}` alone, so routing THAT through this helper added `plan` to flagless `todo` cards.
|
||||
Same shape, different degraded answer — matched here, kept separate there.
|
||||
*/
|
||||
const shouldPrompt = hasStepProgress && isPreImplementationColumnRole(columnFlags, column);
|
||||
let moveOptions: { preserveProgress?: boolean } | undefined;
|
||||
|
||||
if (shouldPrompt) {
|
||||
@@ -555,11 +564,25 @@ function ColumnComponent({ column, tasks, projectId, maxConcurrent, showWorktree
|
||||
}, [shouldPaginate, tasks, visibleTaskCount]);
|
||||
|
||||
const hiddenTaskCount = Math.max(0, tasks.length - visibleTasks.length);
|
||||
const canCreateInColumn = Boolean(
|
||||
onQuickCreate &&
|
||||
!isArchived &&
|
||||
(workflowMode || column === "triage"),
|
||||
);
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-19:45 (Phase B — third attempt, this time with the
|
||||
fixtures migrated instead of the arm defended):
|
||||
The `|| column === "triage"` arm was the LEGACY-board path: before workflow lanes, only the
|
||||
hardcoded intake column offered inline create. U12 deleted the legacy board, Board is Column's
|
||||
only consumer, and it passes `workflowMode` at all three render sites — so the arm is unreachable
|
||||
in production.
|
||||
|
||||
I deleted it twice before and reverted both times, because four Column tests render without
|
||||
`workflowMode` and went red. That was the delete-only rule working: a behaviour change means the
|
||||
branch was not dead FOR THOSE CALLERS. The callers in question are fixtures, not production, so
|
||||
the honest fix is to migrate them to the shape Board actually uses rather than keep an arm alive
|
||||
to satisfy them. Done in Column.test.tsx alongside this.
|
||||
|
||||
Deliberately NOT solved by defaulting `workflowMode` to true: `isArchived`, `isHoldColumn` and
|
||||
`isWipProcessingColumn` all switch on that same flag, so a global default would silently
|
||||
reinterpret every other fixture in the file.
|
||||
*/
|
||||
const canCreateInColumn = Boolean(onQuickCreate && !isArchived && workflowMode);
|
||||
|
||||
const handleQuickCreate = useCallback(
|
||||
(input: TaskCreateInput) => {
|
||||
|
||||
@@ -810,7 +810,7 @@ describe("Column pagination", () => {
|
||||
describe("Column QuickEntryBox", () => {
|
||||
it("renders QuickEntryBox in triage column when onQuickCreate is provided", () => {
|
||||
const tasks = [makeTask("FN-001")];
|
||||
render(<Column {...defaultProps} tasks={tasks} onQuickCreate={vi.fn()} />);
|
||||
render(<Column {...defaultProps} workflowMode columnFlags={{ intake: true }} tasks={tasks} onQuickCreate={vi.fn()} />);
|
||||
expect(screen.getByTestId("quick-entry-box")).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -828,14 +828,14 @@ describe("Column QuickEntryBox", () => {
|
||||
|
||||
it("passes autoExpand={false} to QuickEntryBox in triage column (collapsed by default)", () => {
|
||||
const tasks = [makeTask("FN-001")];
|
||||
render(<Column {...defaultProps} tasks={tasks} onQuickCreate={vi.fn()} />);
|
||||
render(<Column {...defaultProps} workflowMode columnFlags={{ intake: true }} tasks={tasks} onQuickCreate={vi.fn()} />);
|
||||
const quickEntry = screen.getByTestId("quick-entry-box");
|
||||
expect(quickEntry.getAttribute("data-auto-expand")).toBe("false");
|
||||
});
|
||||
|
||||
it("wires QuickEntry Start moves through the host state-updating callback", async () => {
|
||||
const onMoveTask = vi.fn().mockResolvedValue(makeTask("FN-created"));
|
||||
render(<Column {...defaultProps} tasks={[]} onQuickCreate={vi.fn()} onMoveTask={onMoveTask} />);
|
||||
render(<Column {...defaultProps} workflowMode columnFlags={{ intake: true }} tasks={[]} onQuickCreate={vi.fn()} onMoveTask={onMoveTask} />);
|
||||
fireEvent.click(screen.getByTestId("quick-entry-move"));
|
||||
await waitFor(() => expect(onMoveTask).toHaveBeenCalledWith("FN-created", "todo"));
|
||||
});
|
||||
@@ -1331,6 +1331,8 @@ describe("Column same-column drop", () => {
|
||||
<Column
|
||||
{...defaultProps}
|
||||
column="triage"
|
||||
workflowMode
|
||||
columnFlags={{ intake: true }}
|
||||
tasks={[]}
|
||||
onQuickCreate={vi.fn().mockResolvedValue({})}
|
||||
favoriteProviders={["anthropic"]}
|
||||
@@ -1352,6 +1354,8 @@ describe("Column same-column drop", () => {
|
||||
<Column
|
||||
{...defaultProps}
|
||||
column="triage"
|
||||
workflowMode
|
||||
columnFlags={{ intake: true }}
|
||||
tasks={[]}
|
||||
onQuickCreate={vi.fn().mockResolvedValue({})}
|
||||
/>,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Task } from "@fusion/core";
|
||||
import { getUnifiedTaskProgress } from "./taskProgress";
|
||||
import { isIntakeColumnRole, isPreImplementationColumnRole } from "./columnRoles";
|
||||
|
||||
/** The shared status vocabulary for active task phases and lock/model policy. */
|
||||
export const ACTIVE_STATUSES = new Set([
|
||||
@@ -84,9 +85,25 @@ export function isTaskAgentActive(
|
||||
"intake lane, or a hold lane that is replanning"; without them it falls back to the
|
||||
ids, which is the same shape the two lanes have today.
|
||||
*/
|
||||
/*
|
||||
FNXC:WorkflowResolvedColumns 2026-07-30-20:15 (Phase B — one shared predicate):
|
||||
The degraded arm now composes `utils/columnRoles`' predicates instead of naming ids, so the legacy
|
||||
id list lives in exactly one place. Equivalent by construction rather than by inspection:
|
||||
|
||||
intake lane isIntakeColumnRole(undefined, col) -> `triage`
|
||||
hold lane preImplementation AND NOT intake -> `todo`
|
||||
|
||||
which reproduces `col === "triage" || (col === "todo" && isReplanning)` exactly, because the
|
||||
shared pre-implementation set is {todo, triage} and the shared intake id is `triage`.
|
||||
|
||||
Expressed as "not the intake lane" rather than a second id list, so if either shared set changes
|
||||
this composition follows it instead of silently disagreeing with the file next door.
|
||||
*/
|
||||
const isLegacyIntakeLane = isIntakeColumnRole(undefined, task.column);
|
||||
const isLegacyHoldLane = isPreImplementationColumnRole(undefined, task.column) && !isLegacyIntakeLane;
|
||||
const inPlannerLane = options.columnFlags
|
||||
? options.columnFlags.intake === true || (options.columnFlags.hold === true && isReplanning)
|
||||
: task.column === "triage" || (task.column === "todo" && isReplanning);
|
||||
: isLegacyIntakeLane || (isLegacyHoldLane && isReplanning);
|
||||
const hasFreshPlannerActivity = inPlannerLane
|
||||
&& Number.isFinite(recentPlannerActivityAtMs)
|
||||
&& nowMs - recentPlannerActivityAtMs >= 0
|
||||
|
||||
Reference in New Issue
Block a user