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
This commit is contained in:
Fusion
2026-05-04 07:05:08 -07:00
committed by gsxdsm
parent 1bd32a542d
commit 52e74d48c9
4 changed files with 114 additions and 6 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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) {

View File

@@ -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" });