authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-07 17:12:33-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-08 00:12:33+00:00
logd081ab7abc16a21570d3d96948acb6c1f8d847a4
treecbea2a76ce3a6a095ac1fd1ac6916354c58f977f
parente0e0bcb115fa29a2afc670b817fcb4463b620923
signature Signed by PGP key B5690EEEBB952194

fix(web): open pull request markdown links in the panel (#10623)


4 files changed, 49 insertions(+), 9 deletions(-)

apps/web/src/components/ChatMarkdown.tsx+10-2
......@@ -186,6 +186,8 @@ interface ChatMarkdownProps {
186186 text: string;
187187 cwd: string | undefined;
188188 threadRef?: ScopedThreadRef | undefined;
189 /** Panel that receives pull request links, including the standalone PR view. */
190 pullRequestPanelRef?: ScopedThreadRef | undefined;
189191 /** Environment that owns non-thread markdown, such as a pull request panel. */
190192 environmentId?: EnvironmentId | undefined;
191193 onTaskListChange?: ((input: { markerOffset: number; checked: boolean }) => void) | undefined;
......@@ -2160,6 +2162,7 @@ function useChatMarkdownState({
21602162 text,
21612163 cwd,
21622164 threadRef,
2165 pullRequestPanelRef,
21632166 environmentId: explicitEnvironmentId,
21642167 onTaskListChange,
21652168 isStreaming = false,
......@@ -2320,7 +2323,7 @@ function useChatMarkdownState({
23202323 event.clipboardData.setData("text/plain", payload.text);
23212324 event.clipboardData.setData("text/html", payload.html);
23222325 }, []);
2323 const openChangeRequestLink = useOpenChangeRequestLink(threadRef);
2326 const openChangeRequestLink = useOpenChangeRequestLink(threadRef, pullRequestPanelRef);
23242327 const openDeferredMarkdownLink = useOpenLink(threadRef);
23252328 // Subscribed rather than read at click time: the anchor has to decide
23262329 // synchronously whether to intercept its `_blank`, and a subscription is what
......@@ -2815,7 +2818,12 @@ const CHAT_MARKDOWN_COMPONENTS = {
28152818 // A link to a change request in a workspace project opens beside the
28162819 // conversation instead of in a browser: it is the thing being talked about, and
28172820 // the panel it opens offers the browser as one of its actions.
2818 if (!href || openChangeRequestLink(event, href)) return;
2821 if (
2822 !href ||
2823 openChangeRequestLink(event, href, undefined, environmentId ?? undefined)
2824 ) {
2825 return;
2826 }
28192827 // Anything else follows the "Open links in" setting. The system browser
28202828 // keeps the `_blank` the shell already handles; the in-app browser needs
28212829 // the click intercepted here. A modifier click is the way out of the
apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx+5-1
......@@ -659,6 +659,10 @@ export function PullRequestDetailPanel({
659659 if (detail?.autoMergeMethod !== undefined) setMergeMethod(detail.autoMergeMethod);
660660 }, [detail?.autoMergeMethod, pullRequestKey]);
661661 const repositoryUrl = detail === null ? null : changeRequestRepositoryUrl(detail.url);
662 const markdownContext = useMemo(
663 () => ({ repositoryUrl: detail?.provider === "github" ? repositoryUrl : null, threadRef }),
664 [detail?.provider, repositoryUrl, threadRef],
665 );
662666 const authorProfileUrl =
663667 detail?.provider === "github" &&
664668 detail.author !== null &&
......@@ -2328,7 +2332,7 @@ export function PullRequestDetailPanel({
23282332 {...(unavailableGitHubUrl ? { gitHubUrl: unavailableGitHubUrl } : {})}
23292333 />
23302334 ) : detail ? (
2331 <PullRequestMarkdownContext value={detail.provider === "github" ? repositoryUrl : null}>
2335 <PullRequestMarkdownContext value={markdownContext}>
23322336 {mountedTabs.has("summary") ? (
23332337 <div className={cn("absolute inset-0", tab !== "summary" && "invisible")}>
23342338 <PullRequestSummaryTab
apps/web/src/components/pullRequest/PullRequestMarkdown.tsx+10-3
......@@ -4,12 +4,16 @@ import { createContext, useContext, useMemo } from "react";
44import type { Options as ReactMarkdownOptions } from "react-markdown";
55
66import { cn } from "~/lib/utils";
7import { PULL_REQUESTS_PANEL_REF } from "~/rightPanelStore";
78
89import ChatMarkdown from "../ChatMarkdown";
910import { MediaVideoPlayer } from "../media/MediaVideoPlayer";
1011import { remarkPullRequestAutolinks, splitPullRequestBody } from "./pullRequestMarkdown.logic";
1112
12export const PullRequestMarkdownContext = createContext<string | null>(null);
13export const PullRequestMarkdownContext = createContext<{
14 repositoryUrl: string | null;
15 threadRef: ScopedThreadRef | null;
16} | null>(null);
1317
1418/** Renders PR uploads inline, with retry and an original link when video playback fails. */
1519export function PullRequestMarkdown({
......@@ -27,7 +31,9 @@ export function PullRequestMarkdown({
2731 className?: string;
2832}) {
2933 const segments = splitPullRequestBody(text);
30 const repositoryUrl = useContext(PullRequestMarkdownContext);
34 const context = useContext(PullRequestMarkdownContext);
35 const repositoryUrl = context?.repositoryUrl;
36 const resolvedThreadRef = threadRef ?? context?.threadRef ?? undefined;
3137 const extraRemarkPlugins = useMemo<NonNullable<ReactMarkdownOptions["remarkPlugins"]>>(
3238 () => (repositoryUrl ? [[remarkPullRequestAutolinks, { repositoryUrl }]] : []),
3339 [repositoryUrl],
......@@ -41,7 +47,8 @@ export function PullRequestMarkdown({
4147 key={segment.id}
4248 text={segment.text}
4349 cwd={cwd}
44 threadRef={threadRef ?? undefined}
50 threadRef={resolvedThreadRef}
51 pullRequestPanelRef={resolvedThreadRef ?? PULL_REQUESTS_PANEL_REF}
4552 environmentId={environmentId}
4653 extraRemarkPlugins={extraRemarkPlugins}
4754 />
apps/web/src/lib/openPullRequestLink.ts+24-3
......@@ -243,6 +243,7 @@ export function shouldOpenPullRequestExternally(
243243
244244export function useOpenChangeRequestLink(
245245 threadRef?: ScopedThreadRef,
246 panelRef?: ScopedThreadRef,
246247): (
247248 event: Pick<
248249 MouseEvent<HTMLElement>,
......@@ -260,6 +261,7 @@ export function useOpenChangeRequestLink(
260261 (event, targetUrl, targetThreadRef, targetEnvironmentId) => {
261262 if (shouldOpenPullRequestExternally(event)) return false;
262263 const resolvedThreadRef = targetThreadRef ?? threadRef;
264 const resolvedPanelRef = panelRef ?? resolvedThreadRef;
263265 const parsed = parseChangeRequestUrl(targetUrl);
264266 if (parsed === null) return false;
265267 const reads = (environmentId: string) =>
......@@ -286,14 +288,33 @@ export function useOpenChangeRequestLink(
286288 if (project === undefined || !reads(project.environmentId)) return false;
287289 event.preventDefault();
288290 event.stopPropagation();
289 if (resolvedThreadRef) {
290 useRightPanelStore.getState().openPullRequest(resolvedThreadRef, {
291 if (resolvedPanelRef) {
292 useRightPanelStore.getState().openPullRequest(resolvedPanelRef, {
293 // The standalone PR panel has a synthetic ref; each tab keeps its real environment.
294 ...(resolvedPanelRef.environmentId === project.environmentId
295 ? {}
296 : { environmentId: project.environmentId }),
291297 projectId: project.id,
292298 // The identity's own spelling, not the one read out of the URL: the panel asks the
293299 // provider for this repository, while matching a link only ever compares lower case.
294300 repository: project.repositoryIdentity?.displayName ?? parsed.repository,
295301 number: parsed.number,
296302 });
303 if (!resolvedThreadRef) {
304 void navigate({
305 to: "/pull-requests",
306 search: (previous) => ({
307 ...previous,
308 involvement: previous.involvement ?? "all",
309 state: previous.state ?? "all",
310 repository: project.repositoryIdentity?.displayName ?? parsed.repository,
311 number: parsed.number,
312 selectedProjectId: project.id,
313 selectedEnvironmentId: project.environmentId,
314 }),
315 replace: true,
316 });
317 }
297318 return true;
298319 }
299320 void navigate({
......@@ -312,7 +333,7 @@ export function useOpenChangeRequestLink(
312333 });
313334 return true;
314335 },
315 [allProjects, navigate, primaryEnvironmentId, serverConfigs, threadRef],
336 [allProjects, navigate, panelRef, primaryEnvironmentId, serverConfigs, threadRef],
316337 );
317338}
318339