Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions ui/src/apps/issue-write/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
} from "@primer/octicons-react";
import { AppProvider } from "../../components/AppProvider";
import { useMcpApp } from "../../hooks/useMcpApp";
import { completedToolResult } from "../../lib/toolResult";
import { completedToolResult, hasExactIssueReference, parseNumberFromURL } from "../../lib/toolResult";
import { parseUIGetResult } from "../../lib/uiGetResult";
import { MarkdownEditor } from "../../components/MarkdownEditor";

Expand Down Expand Up @@ -294,7 +294,11 @@ function SuccessView({
isUpdate: boolean;
openLink: (url: string) => Promise<void>;
}) {
const issueUrl = issue.html_url || issue.url || issue.URL || "#";
const rawUrl = issue.html_url || issue.url || issue.URL;
const issueNum = issue.number ?? parseNumberFromURL(rawUrl);
const issueUrl = rawUrl || (owner && repo && issueNum ? `https://github.com/${owner}/${repo}/issues/${issueNum}` : "#");
const titleText = issue.title || submittedTitle;
const displayTitle = titleText || (issueNum ? `Issue #${issueNum}` : "View issue");

return (
<div
Expand Down Expand Up @@ -379,16 +383,16 @@ function SuccessView({
textDecoration: "none",
}}
>
{issue.title || submittedTitle}
{issue.number && (
{displayTitle}
{issueNum && titleText && !hasExactIssueReference(titleText, issueNum) && (
<Text
style={{
color: "var(--fgColor-muted)",
fontWeight: 400,
marginLeft: 4,
}}
>
#{issue.number}
#{issueNum}
</Text>
)}
</a>
Expand Down Expand Up @@ -1281,7 +1285,7 @@ function CreateIssueApp() {
issue={shownIssue}
owner={owner}
repo={repo}
submittedTitle={title}
submittedTitle={title || (toolInput?.title as string) || ""}
submittedLabels={selectedLabels}
isUpdate={isUpdateMode}
openLink={openLink}
Expand Down
16 changes: 10 additions & 6 deletions ui/src/apps/pr-edit/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from "@primer/octicons-react";
import { AppProvider } from "../../components/AppProvider";
import { useMcpApp } from "../../hooks/useMcpApp";
import { completedToolResult } from "../../lib/toolResult";
import { completedToolResult, hasExactIssueReference, parseNumberFromURL } from "../../lib/toolResult";
import { parseUIGetResult } from "../../lib/uiGetResult";
import { MarkdownEditor } from "../../components/MarkdownEditor";

Expand Down Expand Up @@ -167,7 +167,11 @@ function SuccessView({
submittedTitle: string;
openLink: (url: string) => Promise<void>;
}) {
const prUrl = pr.html_url || pr.url || pr.URL || "#";
const rawUrl = pr.html_url || pr.url || pr.URL;
const prNumber = pr.number ?? parseNumberFromURL(rawUrl);
const prUrl = rawUrl || (owner && repo && prNumber ? `https://github.com/${owner}/${repo}/pull/${prNumber}` : "#");
const titleText = pr.title || submittedTitle;
const displayTitle = titleText || (prNumber ? `Pull request #${prNumber}` : "View pull request");

return (
<div
Expand Down Expand Up @@ -249,16 +253,16 @@ function SuccessView({
textDecoration: "none",
}}
>
{pr.title || submittedTitle}
{pr.number && (
{displayTitle}
{prNumber && titleText && !hasExactIssueReference(titleText, prNumber) && (
<Text
style={{
color: "var(--fgColor-muted)",
fontWeight: 400,
marginLeft: 4,
}}
>
#{pr.number}
#{prNumber}
</Text>
)}
</a>
Expand Down Expand Up @@ -534,7 +538,7 @@ function EditPRApp() {
if (shownPR) {
return (
<AppProvider hostContext={hostContext}>
<SuccessView pr={shownPR} owner={owner} repo={repo} submittedTitle={submittedTitle} openLink={openLink} />
<SuccessView pr={shownPR} owner={owner} repo={repo} submittedTitle={submittedTitle || title || (toolInput?.title as string) || ""} openLink={openLink} />
</AppProvider>
);
}
Expand Down
20 changes: 13 additions & 7 deletions ui/src/apps/pr-write/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import {
} from "@primer/octicons-react";
import { AppProvider } from "../../components/AppProvider";
import { useMcpApp } from "../../hooks/useMcpApp";
import { completedToolResult } from "../../lib/toolResult";
import { completedToolResult, hasExactIssueReference, parseNumberFromURL } from "../../lib/toolResult";
import { parseUIGetResult } from "../../lib/uiGetResult";
import { MarkdownEditor } from "../../components/MarkdownEditor";

Expand Down Expand Up @@ -80,7 +80,11 @@ function SuccessView({
submittedTitle: string;
openLink: (url: string) => Promise<void>;
}) {
const prUrl = pr.html_url || pr.url || pr.URL || "#";
const rawUrl = pr.html_url || pr.url || pr.URL;
const prNumber = pr.number ?? parseNumberFromURL(rawUrl);
const prUrl = rawUrl || (owner && repo && prNumber ? `https://github.com/${owner}/${repo}/pull/${prNumber}` : "#");
const titleText = pr.title || submittedTitle;
const displayTitle = titleText || (prNumber ? `Pull request #${prNumber}` : "View pull request");

return (
<div
Expand Down Expand Up @@ -165,16 +169,16 @@ function SuccessView({
textDecoration: "none",
}}
>
{pr.title || submittedTitle}
{pr.number && (
{displayTitle}
{prNumber && titleText && !hasExactIssueReference(titleText, prNumber) && (
<Text
style={{
color: "var(--fgColor-muted)",
fontWeight: 400,
marginLeft: 4,
}}
>
#{pr.number}
#{prNumber}
</Text>
)}
</a>
Expand Down Expand Up @@ -457,14 +461,16 @@ function CreatePRApp() {
if (textContent && textContent.type === "text" && textContent.text) {
const prData = JSON.parse(textContent.text);
setSuccessPR(prData);
const createdPRNumber = prData.number ?? parseNumberFromURL(prData.html_url || prData.url || prData.URL);
const prRef = createdPRNumber ? `Pull request #${createdPRNumber}` : "A new pull request";
// Push the new PR into the model context so subsequent agent
// turns can reference it (MCP Apps 2026-01-26 ui/update-model-context).
void setModelContext({
structuredContent: prData,
content: [
{
type: "text",
text: `A new pull request was created in ${owner}/${repo} by the user via the create-pull-request view.`,
text: `${prRef} was created in ${owner}/${repo} by the user via the create-pull-request view.`,
},
],
});
Expand All @@ -480,7 +486,7 @@ function CreatePRApp() {
if (shownPR) {
return (
<AppProvider hostContext={hostContext}>
<SuccessView pr={shownPR} owner={owner} repo={repo} submittedTitle={submittedTitle} openLink={openLink} />
<SuccessView pr={shownPR} owner={owner} repo={repo} submittedTitle={submittedTitle || title || (toolInput?.title as string) || ""} openLink={openLink} />
</AppProvider>
);
}
Expand Down
62 changes: 62 additions & 0 deletions ui/src/lib/toolResult.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { completedToolResult, hasExactIssueReference, parseNumberFromURL } from "./toolResult.ts";

test("completedToolResult: handles null, error, deferral, and completed results", () => {
assert.equal(completedToolResult(null), null);
assert.equal(completedToolResult({ isError: true, content: [{ type: "text", text: "{}" }] }), null);
assert.equal(
completedToolResult({
isError: false,
structuredContent: { status: "awaiting_user_submission" },
content: [{ type: "text", text: "{}" }],
}),
null,
);
assert.equal(completedToolResult({ isError: false, content: [] }), null);
assert.equal(completedToolResult({ isError: false, content: [{ type: "text", text: "invalid-json" }] }), null);
assert.deepEqual(
completedToolResult({
isError: false,
content: [{ type: "text", text: '{"id":"123","url":"https://github.com/owner/repo/pull/42"}' }],
}),
{ id: "123", url: "https://github.com/owner/repo/pull/42" },
);
});

test("parseNumberFromURL: extracts PR numbers from various GitHub URLs", () => {
assert.equal(parseNumberFromURL("https://github.com/owner/repo/pull/123"), 123);
assert.equal(parseNumberFromURL("https://github.com/owner/repo/pull/123/"), 123);
assert.equal(parseNumberFromURL("https://github.com/owner/repo/pull/123/files"), 123);
assert.equal(parseNumberFromURL("https://github.com/owner/repo/pull/123#issuecomment-456"), 123);
assert.equal(parseNumberFromURL("https://github.com/owner/repo/pull/123?diff=unified"), 123);
});

test("parseNumberFromURL: extracts issue numbers from various GitHub URLs", () => {
assert.equal(parseNumberFromURL("https://github.com/owner/repo/issues/789"), 789);
assert.equal(parseNumberFromURL("https://github.com/owner/repo/issues/789/"), 789);
assert.equal(parseNumberFromURL("https://github.com/owner/repo/issues/789#issuecomment-999"), 789);
assert.equal(parseNumberFromURL("https://github.com/owner/repo/issues/789?foo=bar"), 789);
});

test("parseNumberFromURL: returns undefined for non-PR/issue URLs or invalid inputs", () => {
assert.equal(parseNumberFromURL(undefined), undefined);
assert.equal(parseNumberFromURL(""), undefined);
assert.equal(parseNumberFromURL("#"), undefined);
assert.equal(parseNumberFromURL("https://github.com/owner/repo"), undefined);
assert.equal(parseNumberFromURL("https://github.com/owner/repo/pull/notanumber"), undefined);
assert.equal(parseNumberFromURL("https://github.com/owner/repo/issues/notanumber"), undefined);
});

test("hasExactIssueReference: avoids false matches on longer numbers", () => {
assert.equal(hasExactIssueReference("Fix #1234", 123), false);
assert.equal(hasExactIssueReference("Fix #123", 123), true);
assert.equal(hasExactIssueReference("#123", 123), true);
assert.equal(hasExactIssueReference("Fix (#123)", 123), true);
assert.equal(hasExactIssueReference("Issue #456 is fixed", 456), true);
assert.equal(hasExactIssueReference("Issue #4567 is fixed", 456), false);
assert.equal(hasExactIssueReference("Prefix #456", 456), true);
assert.equal(hasExactIssueReference("Prefix #4567", 456), false);
assert.equal(hasExactIssueReference(undefined, 123), false);
assert.equal(hasExactIssueReference("Fix issue", undefined), false);
});
24 changes: 24 additions & 0 deletions ui/src/lib/toolResult.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,27 @@ export function completedToolResult<T = Record<string, unknown>>(
return null;
}
}

/**
* Extracts a numeric PR or issue number from a GitHub web URL, e.g.
* "https://github.com/owner/repo/pull/123" -> 123
* "https://github.com/owner/repo/issues/456" -> 456
*/
export function parseNumberFromURL(url?: string): number | undefined {
if (!url) return undefined;
const match = url.match(/\/(?:pull|issues)\/(\d+)(?:[/?#]|$)/);
return match ? parseInt(match[1], 10) : undefined;
}

/**
* Checks whether a title contains an exact whole-number reference to a GitHub
* issue or pull request (e.g. "#123", "(#123)", "Fix #123"), avoiding false
* positive matches against longer numbers like "#1234".
*/
export function hasExactIssueReference(
title: string | undefined,
number: number | undefined,
): boolean {
if (!title || number === undefined) return false;
return new RegExp(`(?:^|[^\\d])#${number}(?!\\d)`).test(title);
}