FN-7183: link PR numbers to GitHub

Make task-detail PR numbers open their GitHub pull request when a URL is present.

- Render the PR number as a secure external link when prInfo.url is available.
- Keep the existing plain text PR number fallback when no URL is present.
- Add focus/hover styling, dashboard documentation, regression tests, and a patch changeset.

Files changed:
 .changeset/fn-7183-pr-number-link.md               |  7 +++++++
 docs/dashboard-guide.md                            |  1 +
 packages/dashboard/app/components/PrPanel.css      | 13 ++++++++++++
 packages/dashboard/app/components/PrPanel.tsx      | 12 ++++++++++-
 .../app/components/__tests__/PrPanel.test.tsx      | 24 ++++++++++++++++++++++
 5 files changed, 56 insertions(+), 1 deletion(-)

Fusion-Task-Id: FN-7183

Fusion-Task-Lineage: 0a349723-9dda-4784-b747-b140b85a3013

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-06-27 23:37:24 -07:00
parent 79239779cc
commit 27b0cbb05a
5 changed files with 56 additions and 1 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: The PR number in a task's Pull Request tab now links to the pull request on GitHub.
category: feature
dev: PrCard (PrPanel.tsx) wraps the pr-number in an anchor to prInfo.url (new tab, rel=noopener); plain-span fallback when no URL.

View File

@@ -997,6 +997,7 @@ Inspect task definition, logs, review feedback, comments, artifacts, workflow ou
- In shared task edit/create forms, GitHub Tracking appears at the bottom of **More options**, after **Workflow Steps**.
- From this section you can explicitly enable/disable tracking and manage a per-task repo override (`owner/repo`). Clearing the override saves `null` and falls back to project/global defaults.
- In `in-review`, pull-request controls/status (including stall badges) are in a dedicated **Pull Request** tab instead of the Definition tab.
- In the task detail **Pull Request** tab, PR numbers open the linked pull request on GitHub when a PR URL is available.
- Task Detail and list split-pane PR affordances follow the live project auto-merge setting: when auto-merge is off, manual **Create PR** / merge actions are shown; when it is on, the tab shows the automatic auto-merge hint unless a per-task override changes the effective behavior.
- The **Workflow** tab resolves the effective workflow for both explicitly selected and default-inherited tasks. Its overview, expandable graph preview, configured step details, and live step results refresh when switching tasks or projects without showing stale rows from the previous task.
- The **Create Pull Request** modal now offers in-app remediation for every blocking preflight check. If `branchOnRemote` is false, use **Push branch to remote** and Fusion will publish `fusion/<task-id-lower>` to `origin` and refresh preflight. If `conflictsWithBase` is true, use **Resolve conflicts with AI** and Fusion will use an AI coding agent to resolve merge markers on the task branch, commit and push real merge changes, or report success without an empty commit when the selected base is already merged; preflight then refreshes so normal PR creation can continue once all checks pass.

View File

@@ -143,6 +143,19 @@
font-size: 0.75rem;
}
.pr-card a.pr-number {
text-decoration: none;
}
.pr-card a.pr-number:hover {
text-decoration: underline;
}
.pr-card a.pr-number:focus-visible {
box-shadow: var(--focus-ring-strong);
outline: 0;
}
.pr-panel-refresh-icon--muted {
opacity: 0.6;
}

View File

@@ -215,6 +215,8 @@ function PrCard({
const hasConflictBlockingReason = blockingReasons.some((reason) => reason.toLowerCase().includes("conflict"));
const showConflictHint = prInfo.mergeable === "conflicting" || hasConflictBlockingReason;
const conflictDiagnostics = refreshState?.conflictDiagnostics ?? prInfo.conflictDiagnostics;
const prUrl = prInfo.url?.trim();
const prNumberLinkLabel = t("git.viewPrNumberOnGithub", "View PR #{{number}} on GitHub", { number: prInfo.number });
useEffect(() => {
setConflictsExpanded((conflictDiagnostics?.conflictingFiles.length ?? 0) > 0);
@@ -225,7 +227,15 @@ function PrCard({
<div className="pr-header">
<span className="pr-status-icon">{statusIcon}</span>
<span className={`pr-status-badge pr-status-badge--${prInfo.status}`}>{prInfo.status}</span>
<span className="pr-number">#{prInfo.number}</span>
{/*
FNXC:PullRequests 2026-06-27-23:21:
The task-detail Pull Request tab should let operators open GitHub directly from the PR number while preserving a plain-span fallback when no URL exists.
*/}
{prUrl ? (
<a className="pr-number" href={prUrl} target="_blank" rel="noopener noreferrer" aria-label={prNumberLinkLabel} title={prNumberLinkLabel}>#{prInfo.number}</a>
) : (
<span className="pr-number">#{prInfo.number}</span>
)}
<div className="pr-spacer" />
<button className="btn btn-sm pr-refresh-btn" onClick={handleRefresh} disabled={isRefreshing} title={t("git.refreshPrStatus", "Refresh PR status")}>
<RefreshCw size={14} className={isRefreshing ? "spin pr-panel-refresh-icon--muted" : undefined} />

View File

@@ -141,6 +141,30 @@ describe("PrPanel", () => {
expect(screen.getByRole("link", { name: /View on GitHub/i })).toBeInTheDocument();
});
it("links the PR number to GitHub when a PR URL is available", () => {
render(<PrPanel taskId="FN-001" prInfo={mockPrInfo} prAuthAvailable={true} onPrUpdated={mockOnPrUpdated} addToast={mockAddToast} />);
const numberLink = screen.getByRole("link", { name: "View PR #42 on GitHub" });
expect(numberLink).toHaveTextContent("#42");
expect(numberLink).toHaveAttribute("href", mockPrInfo.url);
expect(numberLink).toHaveAttribute("target", "_blank");
expect(numberLink.getAttribute("rel")).toContain("noopener");
expect(numberLink.getAttribute("rel")).toContain("noreferrer");
});
it("keeps the PR number as a plain span when the PR URL is empty", () => {
render(<PrPanel taskId="FN-001" prInfo={{ ...mockPrInfo, url: "" }} prAuthAvailable={true} onPrUpdated={mockOnPrUpdated} addToast={mockAddToast} />);
expect(screen.queryByRole("link", { name: "View PR #42 on GitHub" })).toBeNull();
expect(screen.getByText("#42").tagName).toBe("SPAN");
});
it.each(["open", "merged", "closed"] as const)("links the PR number for status %s", (status) => {
render(<PrPanel taskId="FN-001" prInfo={{ ...mockPrInfo, status }} prAuthAvailable={true} onPrUpdated={mockOnPrUpdated} addToast={mockAddToast} />);
expect(screen.getByRole("link", { name: "View PR #42 on GitHub" })).toHaveAttribute("href", mockPrInfo.url);
});
it("refreshes PR status and updates toast/callback", async () => {
(refreshPrStatus as ReturnType<typeof vi.fn>).mockResolvedValue({
prInfo: { ...mockPrInfo, status: "merged" },