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}