From 52e74d48c9a27db4d797c37defab45260020066b Mon Sep 17 00:00:00 2001 From: Fusion Date: Mon, 4 May 2026 07:05:08 -0700 Subject: [PATCH] feat(FN-3364): sort done tasks by most recent completion - Sort Done column and list views by most recent completion timestamp to keep recent work visible first - Update dashboard and task management docs to document done-column recency ordering behavior - Preserve merge-active task state across verification bounce paths and board/list rendering updates - Forward removeDependencyReferences in deleteTask flows and treat finish_reason=repeat as a soft-stop in engine executor Fusion-Task-Id: FN-3364 --- docs/dashboard-guide.md | 1 + docs/task-management.md | 4 + packages/dashboard/app/components/Board.tsx | 18 +++- .../app/components/__tests__/Board.test.tsx | 97 ++++++++++++++++++- 4 files changed, 114 insertions(+), 6 deletions(-) diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 534b7cf3c..549c72223 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -16,6 +16,7 @@ Features: - Inline quick entry creation - PR/issue badges with live updates - GitHub provenance marker on task cards imported from GitHub (`sourceType: github_import`), shown alongside existing footer metadata like timers +- Column ordering semantics: `triage`, `todo`, `in-progress`, `in-review`, and `archived` stay priority-ordered; `done` is ordered by most recent completion first (`columnMovedAt`, then `updatedAt`, then `createdAt` fallback) ![Board view](./screenshots/dashboard-overview.png) diff --git a/docs/task-management.md b/docs/task-management.md index e49b05f71..c35c250c2 100644 --- a/docs/task-management.md +++ b/docs/task-management.md @@ -86,6 +86,10 @@ Fusion task columns: 5. **done** — merged/finalized 6. **archived** — preserved history, optionally cleaned from filesystem +Board ordering behavior: +- Active work columns (`triage`, `todo`, `in-progress`, `in-review`) remain priority-ordered. +- The `done` column is recency-ordered by completion time (newest first), using `columnMovedAt` as primary and falling back to `updatedAt` then `createdAt` for legacy tasks. + ### Lifecycle commands ```bash diff --git a/packages/dashboard/app/components/Board.tsx b/packages/dashboard/app/components/Board.tsx index 63ae171a5..36aedf2d7 100644 --- a/packages/dashboard/app/components/Board.tsx +++ b/packages/dashboard/app/components/Board.tsx @@ -82,8 +82,24 @@ function compareTaskIdNumeric(a: string, b: string): number { return a.localeCompare(b); } +function getDoneSortTimestamp(task: Task): number { + const timestamp = task.columnMovedAt ?? task.updatedAt ?? task.createdAt; + const parsed = Date.parse(timestamp); + return Number.isFinite(parsed) ? parsed : 0; +} + function sortTasksForColumn(tasks: Task[], column: ColumnType): Task[] { return [...tasks].sort((a, b) => { + if (column === "done") { + const timestampCmp = getDoneSortTimestamp(b) - getDoneSortTimestamp(a); + if (timestampCmp !== 0) { + return timestampCmp; + } + + // Deterministic tie-breaker when completion timestamps match. + return compareTaskIdNumeric(a.id, b.id); + } + // In the in-review column, merging tasks stay pinned above non-merging tasks. if (column === "in-review") { const aIsMerging = a.status === "merging" || a.status === "merging-pr" || a.status === "merging-fix"; @@ -93,7 +109,7 @@ function sortTasksForColumn(tasks: Task[], column: ColumnType): Task[] { } } - // Primary sort: priority descending (urgent → high → normal → low). + // Primary sort for non-done columns: priority descending (urgent → high → normal → low). // compareTaskPriority normalizes missing/invalid values to `normal`. const priorityCmp = compareTaskPriority(a.priority, b.priority); if (priorityCmp !== 0) { diff --git a/packages/dashboard/app/components/__tests__/Board.test.tsx b/packages/dashboard/app/components/__tests__/Board.test.tsx index b7d1096c3..998cdeb92 100644 --- a/packages/dashboard/app/components/__tests__/Board.test.tsx +++ b/packages/dashboard/app/components/__tests__/Board.test.tsx @@ -292,12 +292,99 @@ describe("Board", () => { }); describe("sortTasksForColumn priority ordering", () => { - it("orders tasks by priority descending (urgent → high → normal → low)", () => { + it("orders done tasks by most recent completion regardless of priority", () => { const tasks: Task[] = [ - createTask({ id: "FN-003", description: "Low task", column: "todo", priority: "low" }), - createTask({ id: "FN-001", description: "Urgent task", column: "todo", priority: "urgent" }), - createTask({ id: "FN-004", description: "Normal task", column: "todo", priority: "normal" }), - createTask({ id: "FN-002", description: "High task", column: "todo", priority: "high" }), + createTask({ + id: "FN-003", + description: "Older urgent done task", + column: "done", + priority: "urgent", + columnMovedAt: "2024-01-01T09:00:00.000Z", + }), + createTask({ + id: "FN-001", + description: "Newest low-priority done task", + column: "done", + priority: "low", + columnMovedAt: "2024-01-01T11:00:00.000Z", + }), + createTask({ + id: "FN-002", + description: "Middle high-priority done task", + column: "done", + priority: "high", + columnMovedAt: "2024-01-01T10:00:00.000Z", + }), + ]; + + renderBoard({ tasks }); + + const doneTasks = JSON.parse(screen.getByTestId("column-done").getAttribute("data-tasks") || "[]") as Task[]; + expect(doneTasks.map((t: Task) => t.id)).toEqual(["FN-001", "FN-002", "FN-003"]); + }); + + it("falls back to updatedAt and createdAt for legacy done tasks missing columnMovedAt", () => { + const tasks: Task[] = [ + createTask({ + id: "FN-010", + description: "Has updatedAt fallback", + column: "done", + updatedAt: "2024-01-01T10:30:00.000Z", + }), + createTask({ + id: "FN-011", + description: "Has createdAt fallback", + column: "done", + createdAt: "2024-01-01T10:45:00.000Z", + }), + createTask({ + id: "FN-012", + description: "Has real completion timestamp", + column: "done", + columnMovedAt: "2024-01-01T11:00:00.000Z", + }), + ]; + + const taskWithCreatedAtOnly = tasks[1]; + delete taskWithCreatedAtOnly.columnMovedAt; + delete taskWithCreatedAtOnly.updatedAt; + + renderBoard({ tasks }); + + const doneTasks = JSON.parse(screen.getByTestId("column-done").getAttribute("data-tasks") || "[]") as Task[]; + expect(doneTasks.map((t: Task) => t.id)).toEqual(["FN-012", "FN-011", "FN-010"]); + }); + + it("keeps non-done columns priority-ordered even when recency differs", () => { + const tasks: Task[] = [ + createTask({ + id: "FN-003", + description: "Low but newest", + column: "todo", + priority: "low", + columnMovedAt: "2024-01-01T12:00:00.000Z", + }), + createTask({ + id: "FN-001", + description: "Urgent but older", + column: "todo", + priority: "urgent", + columnMovedAt: "2024-01-01T10:00:00.000Z", + }), + createTask({ + id: "FN-004", + description: "Normal", + column: "todo", + priority: "normal", + columnMovedAt: "2024-01-01T11:00:00.000Z", + }), + createTask({ + id: "FN-002", + description: "High", + column: "todo", + priority: "high", + columnMovedAt: "2024-01-01T09:00:00.000Z", + }), ]; renderBoard({ tasks, searchQuery: "task" });