FN-7525: add Revert/Undo affordance to Done and Archived task cards

Adds a Revert action so operators can undo landed changes for Done/Archived tasks directly from the board.

- Adds onRevertTask wiring through Board, Column, Lane, ListView, TaskDetailModal, and WorktreeGroup surfaces
- Adds a Revert affordance to TaskCard (Done/Archived states) with confirm UX and CSS
- Adds POST /tasks/:id/revert legacy API route supporting "auto" mode with an AI-undo fallback (mode: "ai") on conflict
- Wires useTasks hook and dashboard MainContent/types to support the new revert action
- Adds new i18n strings for the revert affordance
- Adds a minor changeset for @runfusion/fusion documenting the feature
- Adds/updates tests: TaskCard.test.tsx, board-mobile.test.tsx, api-git.test.ts
- Updates docs/dashboard-guide.md and docs/task-management.md

Files changed:
 .changeset/fn-7525-revert-card-affordance.md       |   7 +
 docs/dashboard-guide.md                            |   2 +-
 docs/task-management.md                            |  10 ++
 packages/dashboard/app/App.tsx                     |   7 +-
 packages/dashboard/app/__tests__/api-git.test.ts   |  51 +++++++
 packages/dashboard/app/api/legacy.ts               |  51 +++++++
 packages/dashboard/app/components/AppModals.tsx    |   5 +-
 packages/dashboard/app/components/Board.tsx        |  10 +-
 packages/dashboard/app/components/Column.tsx       |   8 +-
 packages/dashboard/app/components/Lane.tsx         |   5 +-
 packages/dashboard/app/components/ListView.tsx     |  80 ++++++++++-
 packages/dashboard/app/components/TaskCard.css     |  24 +++-
 packages/dashboard/app/components/TaskCard.tsx     | 127 ++++++++++++++++-
 packages/dashboard/app/components/TaskDetailModal.tsx   |  85 ++++++++++++
 packages/dashboard/app/components/WorktreeGroup.tsx     |   6 +
 packages/dashboard/app/components/__tests__/TaskCard.test.tsx     | 151 +++++++++++++++++++++
 packages/dashboard/app/components/__tests__/board-mobile.test.tsx |  63 +++++++++
 packages/dashboard/app/components/dashboard/MainContent.tsx       |   4 +
 packages/dashboard/app/components/dashboard/types.ts    |   8 ++
 packages/dashboard/app/components/useRightDockController.tsx      |   4 +
 packages/dashboard/app/hooks/useTasks.ts           |  21 ++-
 packages/i18n/locales/en/app.json                  |  11 ++
 22 files changed, 718 insertions(+), 22 deletions(-)

Fusion-Task-Id: FN-7525

Fusion-Task-Lineage: b9e2ca94-4ec6-4443-8e41-cf4828018856

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-05 10:29:42 -07:00
parent 94e9d15e38
commit e7cb2f1fed
22 changed files with 718 additions and 22 deletions

View File

@@ -876,6 +876,57 @@ export function unarchiveTask(id: string, projectId?: string): Promise<Task> {
return api<Task>(withProjectId(`/tasks/${id}/unarchive`, projectId), { method: "POST" });
}
/*
FNXC:TaskRevert 2026-07-05-00:00 (FN-7525):
Client-side contract for `POST /tasks/:id/revert` (route owned by FN-7523/
FN-7524/FN-7547/FN-7548 — see the `FNXC:TaskRevert` block in
`register-task-workflow-routes.ts`). This is a discriminated union, NOT a
`Task` — the source task's column/status is never mutated by this call; the
caller (useTasks' `revertTask` op) refreshes the task list afterward so any
newly-created revert commit / AI-undo task becomes visible, without patching
the source task's column directly.
*/
export interface RevertTaskWorkspaceRepoResult {
repo: string;
classification?: string;
revertCommitSha?: string;
conflicts?: unknown;
alreadyReverted?: boolean;
}
export interface RevertTaskGitResult {
mode: "git";
clean: boolean;
revertCommitSha?: string;
revertCommitShas?: string[];
conflicts?: unknown;
alreadyReverted?: boolean;
unsupported?: boolean;
needsHuman?: boolean;
reason?: string;
workspace?: { repos: RevertTaskWorkspaceRepoResult[] };
}
export interface RevertTaskAiResult {
mode: "ai";
createdTaskId: string;
alreadyOpen?: boolean;
}
export type RevertTaskResult = RevertTaskGitResult | RevertTaskAiResult;
export interface RevertTaskOptions {
mode?: "git" | "ai" | "auto";
granularity?: "squash" | "per-sha";
}
export function revertTask(id: string, projectId?: string, body?: RevertTaskOptions): Promise<RevertTaskResult> {
return api<RevertTaskResult>(withProjectId(`/tasks/${id}/revert`, projectId), {
method: "POST",
body: JSON.stringify(body ?? {}),
});
}
export function archiveAllDone(projectId?: string): Promise<Task[]> {
return api<{ archived: Task[] }>(withProjectId("/tasks/archive-all-done", projectId), { method: "POST" }).then(
(response) => response.archived