diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index d72001eb9ddd..dff486df8767 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -186,6 +186,8 @@ interface ChatMarkdownProps { text: string; cwd: string | undefined; threadRef?: ScopedThreadRef | undefined; + /** Panel that receives pull request links, including the standalone PR view. */ + pullRequestPanelRef?: ScopedThreadRef | undefined; /** Environment that owns non-thread markdown, such as a pull request panel. */ environmentId?: EnvironmentId | undefined; onTaskListChange?: ((input: { markerOffset: number; checked: boolean }) => void) | undefined; @@ -2160,6 +2162,7 @@ function useChatMarkdownState({ text, cwd, threadRef, + pullRequestPanelRef, environmentId: explicitEnvironmentId, onTaskListChange, isStreaming = false, @@ -2320,7 +2323,7 @@ function useChatMarkdownState({ event.clipboardData.setData("text/plain", payload.text); event.clipboardData.setData("text/html", payload.html); }, []); - const openChangeRequestLink = useOpenChangeRequestLink(threadRef); + const openChangeRequestLink = useOpenChangeRequestLink(threadRef, pullRequestPanelRef); const openDeferredMarkdownLink = useOpenLink(threadRef); // Subscribed rather than read at click time: the anchor has to decide // synchronously whether to intercept its `_blank`, and a subscription is what @@ -2815,7 +2818,12 @@ const CHAT_MARKDOWN_COMPONENTS = { // A link to a change request in a workspace project opens beside the // conversation instead of in a browser: it is the thing being talked about, and // the panel it opens offers the browser as one of its actions. - if (!href || openChangeRequestLink(event, href)) return; + if ( + !href || + openChangeRequestLink(event, href, undefined, environmentId ?? undefined) + ) { + return; + } // Anything else follows the "Open links in" setting. The system browser // keeps the `_blank` the shell already handles; the in-app browser needs // the click intercepted here. A modifier click is the way out of the diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx index f8514f83dcdf..733a25a35204 100644 --- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx +++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx @@ -659,6 +659,10 @@ export function PullRequestDetailPanel({ if (detail?.autoMergeMethod !== undefined) setMergeMethod(detail.autoMergeMethod); }, [detail?.autoMergeMethod, pullRequestKey]); const repositoryUrl = detail === null ? null : changeRequestRepositoryUrl(detail.url); + const markdownContext = useMemo( + () => ({ repositoryUrl: detail?.provider === "github" ? repositoryUrl : null, threadRef }), + [detail?.provider, repositoryUrl, threadRef], + ); const authorProfileUrl = detail?.provider === "github" && detail.author !== null && @@ -2328,7 +2332,7 @@ export function PullRequestDetailPanel({ {...(unavailableGitHubUrl ? { gitHubUrl: unavailableGitHubUrl } : {})} /> ) : detail ? ( - + {mountedTabs.has("summary") ? (
(null); +export const PullRequestMarkdownContext = createContext<{ + repositoryUrl: string | null; + threadRef: ScopedThreadRef | null; +} | null>(null); /** * A pull request body, rendered with the app's markdown renderer plus a card for each upload @@ -32,7 +36,9 @@ export function PullRequestMarkdown({ className?: string; }) { const segments = splitPullRequestBody(text); - const repositoryUrl = useContext(PullRequestMarkdownContext); + const context = useContext(PullRequestMarkdownContext); + const repositoryUrl = context?.repositoryUrl; + const resolvedThreadRef = threadRef ?? context?.threadRef ?? undefined; const extraRemarkPlugins = useMemo>( () => (repositoryUrl ? [[remarkPullRequestAutolinks, { repositoryUrl }]] : []), [repositoryUrl], @@ -46,7 +52,8 @@ export function PullRequestMarkdown({ key={segment.id} text={segment.text} cwd={cwd} - threadRef={threadRef ?? undefined} + threadRef={resolvedThreadRef} + pullRequestPanelRef={resolvedThreadRef ?? PULL_REQUESTS_PANEL_REF} environmentId={environmentId} extraRemarkPlugins={extraRemarkPlugins} /> diff --git a/apps/web/src/lib/openPullRequestLink.ts b/apps/web/src/lib/openPullRequestLink.ts index 2d46e3984fd9..ae487b67d3ce 100644 --- a/apps/web/src/lib/openPullRequestLink.ts +++ b/apps/web/src/lib/openPullRequestLink.ts @@ -243,6 +243,7 @@ export function shouldOpenPullRequestExternally( export function useOpenChangeRequestLink( threadRef?: ScopedThreadRef, + panelRef?: ScopedThreadRef, ): ( event: Pick< MouseEvent, @@ -260,6 +261,7 @@ export function useOpenChangeRequestLink( (event, targetUrl, targetThreadRef, targetEnvironmentId) => { if (shouldOpenPullRequestExternally(event)) return false; const resolvedThreadRef = targetThreadRef ?? threadRef; + const resolvedPanelRef = panelRef ?? resolvedThreadRef; const parsed = parseChangeRequestUrl(targetUrl); if (parsed === null) return false; const reads = (environmentId: string) => @@ -286,14 +288,33 @@ export function useOpenChangeRequestLink( if (project === undefined || !reads(project.environmentId)) return false; event.preventDefault(); event.stopPropagation(); - if (resolvedThreadRef) { - useRightPanelStore.getState().openPullRequest(resolvedThreadRef, { + if (resolvedPanelRef) { + useRightPanelStore.getState().openPullRequest(resolvedPanelRef, { + // The standalone PR panel has a synthetic ref; each tab keeps its real environment. + ...(resolvedPanelRef.environmentId === project.environmentId + ? {} + : { environmentId: project.environmentId }), projectId: project.id, // The identity's own spelling, not the one read out of the URL: the panel asks the // provider for this repository, while matching a link only ever compares lower case. repository: project.repositoryIdentity?.displayName ?? parsed.repository, number: parsed.number, }); + if (!resolvedThreadRef) { + void navigate({ + to: "/pull-requests", + search: (previous) => ({ + ...previous, + involvement: previous.involvement ?? "all", + state: previous.state ?? "all", + repository: project.repositoryIdentity?.displayName ?? parsed.repository, + number: parsed.number, + selectedProjectId: project.id, + selectedEnvironmentId: project.environmentId, + }), + replace: true, + }); + } return true; } void navigate({ @@ -312,7 +333,7 @@ export function useOpenChangeRequestLink( }); return true; }, - [allProjects, navigate, primaryEnvironmentId, serverConfigs, threadRef], + [allProjects, navigate, panelRef, primaryEnvironmentId, serverConfigs, threadRef], ); }