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