From 9050ee1a42380d1b6f4cd7d7d81771e361c9aacf Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sun, 28 Jun 2026 02:04:31 -0700 Subject: [PATCH] FN-7185: add PR feedback addressing action Add a dashboard action that starts an AI pass for actionable pull-request review feedback. - Add a lifecycle route that validates linked PR tasks, seeds a ce-resolve-pr-feedback steering prompt, returns in-review tasks to active work, and wakes assigned agents when needed. - Add shared PR feedback gating plus task-card and Review tab buttons with loading states, toasts, styling, docs, and translations. - Cover the route, API client, task card, and Review tab behavior with regression tests. Files changed: .changeset/fn-7185-address-pr-feedback.md | 7 + docs/dashboard-guide.md | 1 + packages/dashboard/app/__tests__/api-tasks.test.ts | 12 ++ packages/dashboard/app/api/legacy.ts | 11 ++ packages/dashboard/app/components/TaskCard.css | 7 +- packages/dashboard/app/components/TaskCard.tsx | 44 +++++- .../dashboard/app/components/TaskReviewTab.tsx | 41 ++++- .../app/components/__tests__/TaskCard.test.tsx | 164 +++++++++++++++++++- .../components/__tests__/TaskReviewTab.test.tsx | 127 ++++++++++++++++ packages/dashboard/app/utils/prFeedback.ts | 22 +++ .../dashboard/src/__tests__/routes-tasks.test.ts | 165 +++++++++++++++++++++ .../src/routes/register-task-workflow-routes.ts | 67 +++++++++ packages/i18n/locales/en/app.json | 10 ++ packages/i18n/locales/es/app.json | 22 ++- packages/i18n/locales/fr/app.json | 22 ++- packages/i18n/locales/ko/app.json | 22 ++- packages/i18n/locales/zh-CN/app.json | 22 ++- packages/i18n/locales/zh-TW/app.json | 22 ++- 18 files changed, 764 insertions(+), 24 deletions(-) Fusion-Task-Id: FN-7185 Fusion-Task-Lineage: b2e98163-4fc6-45c9-8ace-a14eeb096e10 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7185-address-pr-feedback.md | 7 + docs/dashboard-guide.md | 1 + .../dashboard/app/__tests__/api-tasks.test.ts | 12 ++ packages/dashboard/app/api/legacy.ts | 11 ++ .../dashboard/app/components/TaskCard.css | 7 +- .../dashboard/app/components/TaskCard.tsx | 44 ++++- .../app/components/TaskReviewTab.tsx | 41 ++++- .../components/__tests__/TaskCard.test.tsx | 164 ++++++++++++++++- .../__tests__/TaskReviewTab.test.tsx | 127 ++++++++++++++ packages/dashboard/app/utils/prFeedback.ts | 22 +++ .../src/__tests__/routes-tasks.test.ts | 165 ++++++++++++++++++ .../routes/register-task-workflow-routes.ts | 67 +++++++ packages/i18n/locales/en/app.json | 10 ++ packages/i18n/locales/es/app.json | 22 ++- packages/i18n/locales/fr/app.json | 22 ++- packages/i18n/locales/ko/app.json | 22 ++- packages/i18n/locales/zh-CN/app.json | 22 ++- packages/i18n/locales/zh-TW/app.json | 22 ++- 18 files changed, 764 insertions(+), 24 deletions(-) create mode 100644 .changeset/fn-7185-address-pr-feedback.md create mode 100644 packages/dashboard/app/utils/prFeedback.ts diff --git a/.changeset/fn-7185-address-pr-feedback.md b/.changeset/fn-7185-address-pr-feedback.md new file mode 100644 index 0000000000..5401008f05 --- /dev/null +++ b/.changeset/fn-7185-address-pr-feedback.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: Add an "Address PR feedback" button that starts an AI session to resolve PR review comments. +category: feature +dev: New POST /tasks/:id/pr/address-feedback route seeds a ce-resolve-pr-feedback steering prompt and wakes the assigned agent; button gates on linked-PR actionable feedback (commentCount or CHANGES_REQUESTED). diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index f0ea5eb016..ab38cbfa03 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -1009,6 +1009,7 @@ Inspect task definition, logs, review feedback, comments, artifacts, workflow ou - Project Settings → Project Models includes optional **PR title prompt guidance** and **PR description prompt guidance** fields. Blank fields preserve the default Create PR metadata prompt; populated fields append guidance for the generated title or body sections. - The **Artifacts** tab combines task documents written by agents or users with task-scoped registered media artifacts. The gallery uses thumbnail-first image/video cards, image and video previews can expand into a dismissible full-size lightbox, video and audio use native controls, document artifacts show text previews, and generic artifacts open through their media URL. - The **Review** tab is separate from **Comments**: Review shows actionable PR/reviewer feedback and same-task revision controls, while Comments remains the general collaboration thread. +- When a linked PR has actionable comments or a changes-requested decision, **Address PR feedback** appears in the Review tab and on the task card; it starts a same-task AI session to evaluate open PR threads, fix valid issues, reply, and resolve them. - Review comments hide GitHub template HTML comments in both Markdown and Plain modes, show author avatars or User/Bot fallbacks, label Human vs Bot/agent authors, and include All/Human/Bot filtering. - **Request revision** in Review resumes work on the same task ID (no refinement task): `in-progress` tasks get steering injection, while `in-review` tasks are moved back to `in-progress` for the same branch/worktree revision pass. The selected feedback can come from either PR review data or reviewer-agent feedback shown in the tab. - Review supports a manual **Refresh** action in-place: PR mode pulls latest GitHub review state/decision, while direct mode rehydrates reviewer-agent feedback from task agent logs (no GitHub call). diff --git a/packages/dashboard/app/__tests__/api-tasks.test.ts b/packages/dashboard/app/__tests__/api-tasks.test.ts index 62acef6009..daac14534d 100644 --- a/packages/dashboard/app/__tests__/api-tasks.test.ts +++ b/packages/dashboard/app/__tests__/api-tasks.test.ts @@ -75,6 +75,7 @@ import { fetchPluginUiSlots, fetchTaskReviewData, refreshTaskReviewData, + addressPrFeedback, type ProjectInfo, type ProjectHealth, type ActivityFeedEntry, @@ -1231,6 +1232,17 @@ describe("task review data api wrappers", () => { expect.objectContaining({ method: "POST" }) ); }); + + it("addressPrFeedback posts to the project-scoped PR feedback endpoint", async () => { + globalThis.fetch = vi.fn().mockResolvedValue(mockFetchResponse(true, { task: FAKE_DETAIL })) as unknown as typeof fetch; + + await addressPrFeedback("FN-123", "proj-1"); + + expect(globalThis.fetch).toHaveBeenCalledWith( + "/api/tasks/FN-123/pr/address-feedback?projectId=proj-1", + expect.objectContaining({ method: "POST" }) + ); + }); }); describe("deleteTask", () => { diff --git a/packages/dashboard/app/api/legacy.ts b/packages/dashboard/app/api/legacy.ts index 6c6275ad37..e5334c8c45 100644 --- a/packages/dashboard/app/api/legacy.ts +++ b/packages/dashboard/app/api/legacy.ts @@ -347,6 +347,10 @@ export interface ReviseTaskReviewResponse { reviewState: NonNullable; } +export interface AddressPrFeedbackResponse { + task: Task; +} + export interface DuplicateMatch { id: string; title: string; @@ -6517,6 +6521,13 @@ export function reviseTaskReviewItems(taskId: string, selectedItems: SelectedRev }); } +/** Request an AI pass that addresses open pull-request feedback for the task's primary PR. */ +export function addressPrFeedback(taskId: string, projectId?: string): Promise { + return api(withProjectId(`/tasks/${encodeURIComponent(taskId)}/pr/address-feedback`, projectId), { + method: "POST", + }); +} + /** Return task to agent - clear assignee and status, move to todo */ export function returnTaskToAgent(taskId: string, projectId?: string): Promise { return api(withProjectId(`/tasks/${encodeURIComponent(taskId)}/return-to-agent`, projectId), { diff --git a/packages/dashboard/app/components/TaskCard.css b/packages/dashboard/app/components/TaskCard.css index b17018d3af..2ba6d4a305 100644 --- a/packages/dashboard/app/components/TaskCard.css +++ b/packages/dashboard/app/components/TaskCard.css @@ -942,10 +942,15 @@ The execution-time badge is part of the footer's bottom-right chip cluster, so i box-shadow var(--transition-fast); } -.card-create-pr-action:hover { +.card-create-pr-action:hover:not(:disabled) { background: var(--card-hover); } +.card-create-pr-action:disabled { + cursor: wait; + opacity: var(--opacity-disabled); +} + .card-create-pr-action:focus-visible { box-shadow: var(--focus-ring-strong); outline: none; diff --git a/packages/dashboard/app/components/TaskCard.tsx b/packages/dashboard/app/components/TaskCard.tsx index a539d8ec32..be82df9d2b 100644 --- a/packages/dashboard/app/components/TaskCard.tsx +++ b/packages/dashboard/app/components/TaskCard.tsx @@ -12,7 +12,7 @@ import { getErrorMessage, } from "@fusion/core"; import { resolveEffectiveAutoMerge } from "../../../core/src/task-merge"; -import { fetchTaskDetail, uploadAttachment, fetchMission, fetchAgent, type WorkflowFieldDefinition } from "../api"; +import { addressPrFeedback, fetchTaskDetail, uploadAttachment, fetchMission, fetchAgent, type WorkflowFieldDefinition } from "../api"; import { GitHubBadge } from "./GitHubBadge"; import { PrCreateModal } from "./PrCreateModal"; import { ProviderIcon } from "./ProviderIcon"; @@ -30,6 +30,7 @@ import { getTaskAgeStalenessCopy, shouldShowTaskAgeStalenessBadge } from "../uti import { getUnifiedTaskProgress } from "../utils/taskProgress"; import { getPrBadgeModifierClass } from "../utils/prBadgeClass"; import { getActiveRuntimeMs, getEndToEndDurationMs, getTimedDurationMs, getWorkflowRuntimeMs, parseTimestampToMs } from "../utils/taskTiming"; +import { canStartPrFeedbackAddressing, getTaskPrimaryPrInfo } from "../utils/prFeedback"; import type { ToastType } from "../hooks/useToast"; import { useConfirm } from "../hooks/useConfirm"; import { extractDependencyDeleteConflict, extractLineageDeleteConflict } from "../utils/taskDelete"; @@ -442,10 +443,6 @@ export interface CliCardState { | "needsAttention"; } -function getTaskPrimaryPrInfo(task: Pick): PrInfo | undefined { - return task.prInfos?.[0] ?? task.prInfo; -} - function areTaskBadgeInfosEqual( previous: PrInfo | IssueInfo | undefined, next: PrInfo | IssueInfo | undefined, @@ -735,6 +732,7 @@ function TaskCardComponent({ const [showSendBackMenu, setShowSendBackMenu] = useState(false); const [isRetrying, setIsRetrying] = useState(false); const [isPrCreateOpen, setIsPrCreateOpen] = useState(false); + const [isAddressingPrFeedback, setIsAddressingPrFeedback] = useState(false); const [timeIndicatorNowMs, setTimeIndicatorNowMs] = useState(() => Date.now()); const descTextareaRef = useRef(null); @@ -1311,6 +1309,7 @@ function TaskCardComponent({ && !isPaused && !isFailed && !queued; + const showAddressPrFeedbackAction = canStartPrFeedbackAddressing(task); const metaRowVisible = (task.dependencies?.length ?? 0) > 0 || queued @@ -1318,7 +1317,7 @@ function TaskCardComponent({ || Boolean(task.blockedBy) || Boolean(task.overlapBlockedBy) || Boolean(fanout && fanout.totalCount > 0); - const shouldRenderActionRow = Boolean(onPromote) || showCreatePrQuickAction || (showInReviewMoveControl && !metaRowVisible); + const shouldRenderActionRow = Boolean(onPromote) || showCreatePrQuickAction || showAddressPrFeedbackAction || (showInReviewMoveControl && !metaRowVisible); const renderInReviewMoveControl = () => (
@@ -1714,6 +1713,21 @@ function TaskCardComponent({ void onPromote(task.id); }, [isPromoting, onPromote, task.id]); + const handleAddressPrFeedbackClick = useCallback(async (e: React.MouseEvent) => { + e.stopPropagation(); + if (isAddressingPrFeedback) return; + + setIsAddressingPrFeedback(true); + try { + await addressPrFeedback(task.id, projectId); + addToast(t("tasks.addressPrFeedbackStarted", "Addressing PR feedback — AI session started"), "success"); + } catch (err) { + addToast(t("tasks.addressPrFeedbackFailed", "Failed to start PR feedback session: {{error}}", { error: getErrorMessage(err) }), "error"); + } finally { + setIsAddressingPrFeedback(false); + } + }, [addToast, isAddressingPrFeedback, projectId, t, task.id]); + const handleRetryTask = useCallback(async (e: React.MouseEvent) => { e.stopPropagation(); if (!onRetryTask || isRetrying) return; @@ -2498,6 +2512,24 @@ function TaskCardComponent({ {t("tasks.createPr", "Create PR")} )} + {showAddressPrFeedbackAction && ( + + )} {onPromote && ( ) : null} + {canAddressPrFeedback ? ( + <> + {/* + FNXC:TaskReviewPrFeedback 2026-06-28-00:00: + The Review tab must expose the same gated Address PR feedback action as task cards when PR comments or CHANGES_REQUESTED make feedback actionable. Route the click through the lifecycle API so the ce-resolve-pr-feedback steering prompt is visible in Chat and can wake the assigned agent. + */} + + + ) : null}