From 30c9d9ba095a9a8cdb20c3c45f18ad245b17a345 Mon Sep 17 00:00:00 2001 From: Matt Holloway Date: Tue, 2 Jun 2026 12:39:23 +0100 Subject: [PATCH 1/2] Open created issue/PR link via host open-link capability The success views for issue-write and pr-write rendered a plain anchor to the created/updated issue or PR. MCP Apps run in a sandboxed iframe where target="_blank" navigation may be blocked, so clicking the link did nothing in some hosts. Route the click through the host's ui/open-link capability (already exposed by useMcpApp as openLink), which asks the host to open the URL in the user's browser. The hook now also falls back to window.open when the host denies the request, in addition to the existing no-app fallback. The href is retained so right-click/copy and native fallback still work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ui/src/apps/issue-write/App.tsx | 13 ++++++++++++- ui/src/apps/pr-write/App.tsx | 14 ++++++++++++-- ui/src/hooks/useMcpApp.ts | 7 ++++++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/ui/src/apps/issue-write/App.tsx b/ui/src/apps/issue-write/App.tsx index fedb7f24f..3f87c94ad 100644 --- a/ui/src/apps/issue-write/App.tsx +++ b/ui/src/apps/issue-write/App.tsx @@ -33,12 +33,14 @@ function SuccessView({ repo, submittedTitle, isUpdate, + openLink, }: { issue: IssueResult; owner: string; repo: string; submittedTitle: string; isUpdate: boolean; + openLink: (url: string) => Promise; }) { const issueUrl = issue.html_url || issue.url || issue.URL || "#"; @@ -87,6 +89,14 @@ function SuccessView({ href={issueUrl} target="_blank" rel="noopener noreferrer" + onClick={(e) => { + if (issueUrl === "#") return; + // MCP Apps run in a sandboxed iframe where a plain anchor may be + // blocked, so route the click through the host's open-link + // capability (falls back to window.open). + e.preventDefault(); + void openLink(issueUrl); + }} style={{ fontWeight: 600, fontSize: "14px", @@ -121,7 +131,7 @@ function CreateIssueApp() { const [error, setError] = useState(null); const [successIssue, setSuccessIssue] = useState(null); - const { app, error: appError, toolInput, callTool, hostContext, setModelContext } = useMcpApp({ + const { app, error: appError, toolInput, callTool, hostContext, setModelContext, openLink } = useMcpApp({ appName: "github-mcp-server-issue-write", }); @@ -232,6 +242,7 @@ function CreateIssueApp() { repo={repo} submittedTitle={title} isUpdate={isUpdateMode} + openLink={openLink} /> ); } diff --git a/ui/src/apps/pr-write/App.tsx b/ui/src/apps/pr-write/App.tsx index abbeacb12..95c66e277 100644 --- a/ui/src/apps/pr-write/App.tsx +++ b/ui/src/apps/pr-write/App.tsx @@ -36,11 +36,13 @@ function SuccessView({ owner, repo, submittedTitle, + openLink, }: { pr: PRResult; owner: string; repo: string; submittedTitle: string; + openLink: (url: string) => Promise; }) { const prUrl = pr.html_url || pr.url || pr.URL || "#"; @@ -89,6 +91,14 @@ function SuccessView({ href={prUrl} target="_blank" rel="noopener noreferrer" + onClick={(e) => { + if (prUrl === "#") return; + // MCP Apps run in a sandboxed iframe where a plain anchor may be + // blocked, so route the click through the host's open-link + // capability (falls back to window.open). + e.preventDefault(); + void openLink(prUrl); + }} style={{ fontWeight: 600, fontSize: "14px", @@ -126,7 +136,7 @@ function CreatePRApp() { const [isDraft, setIsDraft] = useState(false); const [maintainerCanModify, setMaintainerCanModify] = useState(true); - const { app, error: appError, toolInput, callTool, hostContext, setModelContext } = useMcpApp({ + const { app, error: appError, toolInput, callTool, hostContext, setModelContext, openLink } = useMcpApp({ appName: "github-mcp-server-create-pull-request", }); @@ -199,7 +209,7 @@ function CreatePRApp() { if (successPR) { return ( - + ); } diff --git a/ui/src/hooks/useMcpApp.ts b/ui/src/hooks/useMcpApp.ts index b060ea6ee..cf386520f 100644 --- a/ui/src/hooks/useMcpApp.ts +++ b/ui/src/hooks/useMcpApp.ts @@ -106,7 +106,12 @@ export function useMcpApp({ window.open(url, "_blank", "noopener,noreferrer"); return; } - await app.openLink({ url }); + const result = await app.openLink({ url }); + // The host may deny the request (e.g. blocked domain or user cancelled). + // Fall back to a direct window.open so the link still works. + if (result?.isError) { + window.open(url, "_blank", "noopener,noreferrer"); + } }, [app] ); From dcfe30c49e101a7c489ad34748c3edee89708a70 Mon Sep 17 00:00:00 2001 From: Matt Holloway Date: Tue, 2 Jun 2026 12:43:46 +0100 Subject: [PATCH 2/2] Prevent default anchor navigation before URL check When the success-view link URL was unavailable ("#"), the click handler returned before calling e.preventDefault(), so the anchor's default target="_blank" navigation still ran and could open a stray blank tab. Call preventDefault() first, then no-op when the URL is unavailable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ui/src/apps/issue-write/App.tsx | 2 +- ui/src/apps/pr-write/App.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/apps/issue-write/App.tsx b/ui/src/apps/issue-write/App.tsx index 3f87c94ad..6c46b8c08 100644 --- a/ui/src/apps/issue-write/App.tsx +++ b/ui/src/apps/issue-write/App.tsx @@ -90,11 +90,11 @@ function SuccessView({ target="_blank" rel="noopener noreferrer" onClick={(e) => { - if (issueUrl === "#") return; // MCP Apps run in a sandboxed iframe where a plain anchor may be // blocked, so route the click through the host's open-link // capability (falls back to window.open). e.preventDefault(); + if (issueUrl === "#") return; void openLink(issueUrl); }} style={{ diff --git a/ui/src/apps/pr-write/App.tsx b/ui/src/apps/pr-write/App.tsx index 95c66e277..245753a1b 100644 --- a/ui/src/apps/pr-write/App.tsx +++ b/ui/src/apps/pr-write/App.tsx @@ -92,11 +92,11 @@ function SuccessView({ target="_blank" rel="noopener noreferrer" onClick={(e) => { - if (prUrl === "#") return; // MCP Apps run in a sandboxed iframe where a plain anchor may be // blocked, so route the click through the host's open-link // capability (falls back to window.open). e.preventDefault(); + if (prUrl === "#") return; void openLink(prUrl); }} style={{