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 {...@@ -186,6 +186,8 @@ interface ChatMarkdownProps {
186 text: string;186 text: string;
187 cwd: string | undefined;187 cwd: string | undefined;
188 threadRef?: ScopedThreadRef | undefined;188 threadRef?: ScopedThreadRef | undefined;
189 /** Panel that receives pull request links, including the standalone PR view. */
190 pullRequestPanelRef?: ScopedThreadRef | undefined;
189 /** Environment that owns non-thread markdown, such as a pull request panel. */191 /** Environment that owns non-thread markdown, such as a pull request panel. */
190 environmentId?: EnvironmentId | undefined;192 environmentId?: EnvironmentId | undefined;
191 onTaskListChange?: ((input: { markerOffset: number; checked: boolean }) => void) | undefined;193 onTaskListChange?: ((input: { markerOffset: number; checked: boolean }) => void) | undefined;
...@@ -2160,6 +2162,7 @@ function useChatMarkdownState({...@@ -2160,6 +2162,7 @@ function useChatMarkdownState({
2160 text,2162 text,
2161 cwd,2163 cwd,
2162 threadRef,2164 threadRef,
2165 pullRequestPanelRef,
2163 environmentId: explicitEnvironmentId,2166 environmentId: explicitEnvironmentId,
2164 onTaskListChange,2167 onTaskListChange,
2165 isStreaming = false,2168 isStreaming = false,
...@@ -2320,7 +2323,7 @@ function useChatMarkdownState({...@@ -2320,7 +2323,7 @@ function useChatMarkdownState({
2320 event.clipboardData.setData("text/plain", payload.text);2323 event.clipboardData.setData("text/plain", payload.text);
2321 event.clipboardData.setData("text/html", payload.html);2324 event.clipboardData.setData("text/html", payload.html);
2322 }, []);2325 }, []);
2323 const openChangeRequestLink = useOpenChangeRequestLink(threadRef);2326 const openChangeRequestLink = useOpenChangeRequestLink(threadRef, pullRequestPanelRef);
2324 const openDeferredMarkdownLink = useOpenLink(threadRef);2327 const openDeferredMarkdownLink = useOpenLink(threadRef);
2325 // Subscribed rather than read at click time: the anchor has to decide2328 // Subscribed rather than read at click time: the anchor has to decide
2326 // synchronously whether to intercept its `_blank`, and a subscription is what2329 // synchronously whether to intercept its `_blank`, and a subscription is what
...@@ -2815,7 +2818,12 @@ const CHAT_MARKDOWN_COMPONENTS = {...@@ -2815,7 +2818,12 @@ const CHAT_MARKDOWN_COMPONENTS = {
2815 // A link to a change request in a workspace project opens beside the2818 // A link to a change request in a workspace project opens beside the
2816 // conversation instead of in a browser: it is the thing being talked about, and2819 // conversation instead of in a browser: it is the thing being talked about, and
2817 // the panel it opens offers the browser as one of its actions.2820 // 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 }
2819 // Anything else follows the "Open links in" setting. The system browser2827 // Anything else follows the "Open links in" setting. The system browser
2820 // keeps the `_blank` the shell already handles; the in-app browser needs2828 // keeps the `_blank` the shell already handles; the in-app browser needs
2821 // the click intercepted here. A modifier click is the way out of the2829 // 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({...@@ -659,6 +659,10 @@ export function PullRequestDetailPanel({
659 if (detail?.autoMergeMethod !== undefined) setMergeMethod(detail.autoMergeMethod);659 if (detail?.autoMergeMethod !== undefined) setMergeMethod(detail.autoMergeMethod);
660 }, [detail?.autoMergeMethod, pullRequestKey]);660 }, [detail?.autoMergeMethod, pullRequestKey]);
661 const repositoryUrl = detail === null ? null : changeRequestRepositoryUrl(detail.url);661 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 );
662 const authorProfileUrl =666 const authorProfileUrl =
663 detail?.provider === "github" &&667 detail?.provider === "github" &&
664 detail.author !== null &&668 detail.author !== null &&
...@@ -2328,7 +2332,7 @@ export function PullRequestDetailPanel({...@@ -2328,7 +2332,7 @@ export function PullRequestDetailPanel({
2328 {...(unavailableGitHubUrl ? { gitHubUrl: unavailableGitHubUrl } : {})}2332 {...(unavailableGitHubUrl ? { gitHubUrl: unavailableGitHubUrl } : {})}
2329 />2333 />
2330 ) : detail ? (2334 ) : detail ? (
2331 <PullRequestMarkdownContext value={detail.provider === "github" ? repositoryUrl : null}>2335 <PullRequestMarkdownContext value={markdownContext}>
2332 {mountedTabs.has("summary") ? (2336 {mountedTabs.has("summary") ? (
2333 <div className={cn("absolute inset-0", tab !== "summary" && "invisible")}>2337 <div className={cn("absolute inset-0", tab !== "summary" && "invisible")}>
2334 <PullRequestSummaryTab2338 <PullRequestSummaryTab
apps/web/src/components/pullRequest/PullRequestMarkdown.tsx+10-3
...@@ -4,12 +4,16 @@ import { createContext, useContext, useMemo } from "react";...@@ -4,12 +4,16 @@ import { createContext, useContext, useMemo } from "react";
4import type { Options as ReactMarkdownOptions } from "react-markdown";4import type { Options as ReactMarkdownOptions } from "react-markdown";
55
6import { cn } from "~/lib/utils";6import { cn } from "~/lib/utils";
7import { PULL_REQUESTS_PANEL_REF } from "~/rightPanelStore";
78
8import ChatMarkdown from "../ChatMarkdown";9import ChatMarkdown from "../ChatMarkdown";
9import { MediaVideoPlayer } from "../media/MediaVideoPlayer";10import { MediaVideoPlayer } from "../media/MediaVideoPlayer";
10import { remarkPullRequestAutolinks, splitPullRequestBody } from "./pullRequestMarkdown.logic";11import { 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
14/** Renders PR uploads inline, with retry and an original link when video playback fails. */18/** Renders PR uploads inline, with retry and an original link when video playback fails. */
15export function PullRequestMarkdown({19export function PullRequestMarkdown({
...@@ -27,7 +31,9 @@ export function PullRequestMarkdown({...@@ -27,7 +31,9 @@ export function PullRequestMarkdown({
27 className?: string;31 className?: string;
28}) {32}) {
29 const segments = splitPullRequestBody(text);33 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;
31 const extraRemarkPlugins = useMemo<NonNullable<ReactMarkdownOptions["remarkPlugins"]>>(37 const extraRemarkPlugins = useMemo<NonNullable<ReactMarkdownOptions["remarkPlugins"]>>(
32 () => (repositoryUrl ? [[remarkPullRequestAutolinks, { repositoryUrl }]] : []),38 () => (repositoryUrl ? [[remarkPullRequestAutolinks, { repositoryUrl }]] : []),
33 [repositoryUrl],39 [repositoryUrl],
...@@ -41,7 +47,8 @@ export function PullRequestMarkdown({...@@ -41,7 +47,8 @@ export function PullRequestMarkdown({
41 key={segment.id}47 key={segment.id}
42 text={segment.text}48 text={segment.text}
43 cwd={cwd}49 cwd={cwd}
44 threadRef={threadRef ?? undefined}50 threadRef={resolvedThreadRef}
51 pullRequestPanelRef={resolvedThreadRef ?? PULL_REQUESTS_PANEL_REF}
45 environmentId={environmentId}52 environmentId={environmentId}
46 extraRemarkPlugins={extraRemarkPlugins}53 extraRemarkPlugins={extraRemarkPlugins}
47 />54 />
apps/web/src/lib/openPullRequestLink.ts+24-3
...@@ -243,6 +243,7 @@ export function shouldOpenPullRequestExternally(...@@ -243,6 +243,7 @@ export function shouldOpenPullRequestExternally(
243243
244export function useOpenChangeRequestLink(244export function useOpenChangeRequestLink(
245 threadRef?: ScopedThreadRef,245 threadRef?: ScopedThreadRef,
246 panelRef?: ScopedThreadRef,
246): (247): (
247 event: Pick<248 event: Pick<
248 MouseEvent<HTMLElement>,249 MouseEvent<HTMLElement>,
...@@ -260,6 +261,7 @@ export function useOpenChangeRequestLink(...@@ -260,6 +261,7 @@ export function useOpenChangeRequestLink(
260 (event, targetUrl, targetThreadRef, targetEnvironmentId) => {261 (event, targetUrl, targetThreadRef, targetEnvironmentId) => {
261 if (shouldOpenPullRequestExternally(event)) return false;262 if (shouldOpenPullRequestExternally(event)) return false;
262 const resolvedThreadRef = targetThreadRef ?? threadRef;263 const resolvedThreadRef = targetThreadRef ?? threadRef;
264 const resolvedPanelRef = panelRef ?? resolvedThreadRef;
263 const parsed = parseChangeRequestUrl(targetUrl);265 const parsed = parseChangeRequestUrl(targetUrl);
264 if (parsed === null) return false;266 if (parsed === null) return false;
265 const reads = (environmentId: string) =>267 const reads = (environmentId: string) =>
...@@ -286,14 +288,33 @@ export function useOpenChangeRequestLink(...@@ -286,14 +288,33 @@ export function useOpenChangeRequestLink(
286 if (project === undefined || !reads(project.environmentId)) return false;288 if (project === undefined || !reads(project.environmentId)) return false;
287 event.preventDefault();289 event.preventDefault();
288 event.stopPropagation();290 event.stopPropagation();
289 if (resolvedThreadRef) {291 if (resolvedPanelRef) {
290 useRightPanelStore.getState().openPullRequest(resolvedThreadRef, {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 }),
291 projectId: project.id,297 projectId: project.id,
292 // The identity's own spelling, not the one read out of the URL: the panel asks the298 // The identity's own spelling, not the one read out of the URL: the panel asks the
293 // provider for this repository, while matching a link only ever compares lower case.299 // provider for this repository, while matching a link only ever compares lower case.
294 repository: project.repositoryIdentity?.displayName ?? parsed.repository,300 repository: project.repositoryIdentity?.displayName ?? parsed.repository,
295 number: parsed.number,301 number: parsed.number,
296 });302 });
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 }
297 return true;318 return true;
298 }319 }
299 void navigate({320 void navigate({
...@@ -312,7 +333,7 @@ export function useOpenChangeRequestLink(...@@ -312,7 +333,7 @@ export function useOpenChangeRequestLink(
312 });333 });
313 return true;334 return true;
314 },335 },
315 [allProjects, navigate, primaryEnvironmentId, serverConfigs, threadRef],336 [allProjects, navigate, panelRef, primaryEnvironmentId, serverConfigs, threadRef],
316 );337 );
317}338}
318339