Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
18 changes: 15 additions & 3 deletions ui/src/apps/issue-write/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from "@primer/octicons-react";
import { AppProvider } from "../../components/AppProvider";
import { useMcpApp } from "../../hooks/useMcpApp";
import { completedToolResult } from "../../lib/toolResult";
import { MarkdownEditor } from "../../components/MarkdownEditor";

interface IssueResult {
Expand Down Expand Up @@ -435,10 +436,21 @@ function CreateIssueApp() {
const [repoSearchLoading, setRepoSearchLoading] = useState(false);
const [repoFilter, setRepoFilter] = useState("");

const { app, error: appError, toolInput, callTool, hostContext, setModelContext, openLink } = useMcpApp({
const { app, error: appError, toolInput, toolResult, callTool, hostContext, setModelContext, openLink } = useMcpApp({
appName: "github-mcp-server-issue-write",
});

// When the server created/updated the issue up-front instead of deferring to
// this form (e.g. the agent passed show_ui=false or parameters the form can't
// represent, such as labels/assignees/issue_fields), the host still renders
// this View and delivers the result via tool-result. Render that completed
// result as success so we never show a create/update form for an issue that
// is already done. The deferral sentinel (awaiting_user_submission) returns
// null here, keeping the form for the normal deferred flow.
// See github/copilot-mcp-core#1864.
const resultIssue = useMemo(() => completedToolResult<IssueResult>(toolResult), [toolResult]);
const shownIssue = successIssue ?? resultIssue;
Comment thread
SamMorrowDrums marked this conversation as resolved.

// Get method and issue_number from toolInput
const method = (toolInput?.method as string) || "create";
const issueNumber = toolInput?.issue_number as number | undefined;
Expand Down Expand Up @@ -1210,10 +1222,10 @@ function CreateIssueApp() {
);
}

if (successIssue) {
if (shownIssue) {
return (
<SuccessView
issue={successIssue}
issue={shownIssue}
owner={owner}
repo={repo}
submittedTitle={title}
Expand Down
16 changes: 13 additions & 3 deletions ui/src/apps/pr-edit/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
} from "@primer/octicons-react";
import { AppProvider } from "../../components/AppProvider";
import { useMcpApp } from "../../hooks/useMcpApp";
import { completedToolResult } from "../../lib/toolResult";
import { MarkdownEditor } from "../../components/MarkdownEditor";

interface PRResult {
Expand Down Expand Up @@ -267,14 +268,23 @@ function EditPRApp() {
const [reviewersLoading, setReviewersLoading] = useState(false);
const [reviewersFilter, setReviewersFilter] = useState("");

const { app, error: appError, toolInput, callTool, hostContext, setModelContext, openLink } = useMcpApp({
const { app, error: appError, toolInput, toolResult, callTool, hostContext, setModelContext, openLink } = useMcpApp({
appName: "github-mcp-server-edit-pull-request",
});

const owner = (toolInput?.owner as string) || "";
const repo = (toolInput?.repo as string) || "";
const pullNumber = asNumber(toolInput?.pullNumber);

// When the server updated the PR up-front instead of deferring to this form,
// the host still renders this View and delivers the updated PR via
// tool-result. Render that completed result as success so we never show an
// edit form for changes already applied. The deferral sentinel
// (awaiting_user_submission) returns null here, keeping the form for the
// normal deferred flow. See github/copilot-mcp-core#1864.
const resultPR = useMemo(() => completedToolResult<PRResult>(toolResult), [toolResult]);
const shownPR = successPR ?? resultPR;

Comment on lines +279 to +287

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 09a7a78. Rather than key the parsed result per app, I scoped it centrally in useMcpApp: toolResult is now reset to null whenever a new tool-input notification arrives. The MCP Apps 2026-01-26 spec guarantees tool-input is delivered before that invocation's tool-result, so a new invocation drops any prior result before its own arrives — closing the stale-success window for all three Views without per-app invocation keys.

useEffect(() => {
setTitle("");
setBody("");
Expand Down Expand Up @@ -495,10 +505,10 @@ function EditPRApp() {
}
}, [title, body, owner, repo, pullNumber, baseBranch, initialValues, selectedReviewers, prState, isDraft, maintainerCanModify, callTool, setModelContext]);

if (successPR) {
if (shownPR) {
return (
<AppProvider hostContext={hostContext}>
<SuccessView pr={successPR} owner={owner} repo={repo} submittedTitle={submittedTitle} openLink={openLink} />
<SuccessView pr={shownPR} owner={owner} repo={repo} submittedTitle={submittedTitle} openLink={openLink} />
</AppProvider>
);
}
Expand Down
17 changes: 14 additions & 3 deletions ui/src/apps/pr-write/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
} from "@primer/octicons-react";
import { AppProvider } from "../../components/AppProvider";
import { useMcpApp } from "../../hooks/useMcpApp";
import { completedToolResult } from "../../lib/toolResult";
import { MarkdownEditor } from "../../components/MarkdownEditor";

interface PRResult {
Expand Down Expand Up @@ -189,14 +190,24 @@ function CreatePRApp() {
const [repoSearchLoading, setRepoSearchLoading] = useState(false);
const [repoFilter, setRepoFilter] = useState("");

const { app, error: appError, toolInput, callTool, hostContext, setModelContext, openLink } = useMcpApp({
const { app, error: appError, toolInput, toolResult, callTool, hostContext, setModelContext, openLink } = useMcpApp({
appName: "github-mcp-server-create-pull-request",
});

const owner = selectedRepo?.owner || (toolInput?.owner as string) || "";
const repo = selectedRepo?.name || (toolInput?.repo as string) || "";
const [submittedTitle, setSubmittedTitle] = useState("");

// When the server executed up-front instead of deferring to this form (e.g.
// the agent passed show_ui=false or parameters the form can't represent), the
// host still renders this View and delivers the created PR via tool-result.
// Treat that completed result as a success so we never show a "Create pull
// request" form for a PR that already exists. The deferral sentinel
// (awaiting_user_submission) returns null here, keeping the form for the
// normal deferred flow. See github/copilot-mcp-core#1864.
const resultPR = useMemo(() => completedToolResult<PRResult>(toolResult), [toolResult]);
const shownPR = successPR ?? resultPR;

Comment thread
SamMorrowDrums marked this conversation as resolved.
// Reset all transient form/result state when toolInput changes (new invocation).
// Without this, the SuccessView from a previous submit stays visible and stale
// form values bleed through because the prefill effect below only sets when
Expand Down Expand Up @@ -440,10 +451,10 @@ function CreatePRApp() {
}
}, [title, body, owner, repo, baseBranch, headBranch, isDraft, maintainerCanModify, selectedReviewers, toolInput, callTool, setModelContext]);

if (successPR) {
if (shownPR) {
return (
<AppProvider hostContext={hostContext}>
<SuccessView pr={successPR} owner={owner} repo={repo} submittedTitle={submittedTitle} openLink={openLink} />
<SuccessView pr={shownPR} owner={owner} repo={repo} submittedTitle={submittedTitle} openLink={openLink} />
</AppProvider>
);
}
Expand Down
37 changes: 37 additions & 0 deletions ui/src/lib/toolResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";

/**
* Returns the parsed JSON payload of a *completed* tool result, or `null` when
* the result is absent, an error, or the deferral sentinel (structured
* `status` of `"awaiting_user_submission"`, set by the server's
* `NewToolResultAwaitingFormSubmission`).
*
* Form-backed write Views (create_pull_request, issue_write,
* update_pull_request) use this to tell apart "the server deferred and is
* waiting for my form submission" from "the server already executed". Without
* it, a View renders its input form off in-app state alone and will show e.g. a
* "Create pull request" form for a PR that was already created up-front (when
* the agent passed `show_ui=false` or parameters the form can't represent).
*
* See github/copilot-mcp-core#1864 for the full show/defer state machine.
*/
export function completedToolResult<T = Record<string, unknown>>(
result: CallToolResult | null,
): T | null {
if (!result || result.isError) return null;

const status = (result.structuredContent as { status?: string } | undefined)
?.status;
if (status === "awaiting_user_submission") return null;

const textContent = result.content?.find((c) => c.type === "text");
if (!textContent || textContent.type !== "text" || !textContent.text) {
return null;
}

try {
return JSON.parse(textContent.text) as T;
} catch {
return null;
}
}
Loading