| 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,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 decide | 2328 | // Subscribed rather than read at click time: the anchor has to decide |
| 2326 | // synchronously whether to intercept its `_blank`, and a subscription is what | 2329 | // 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 the | 2818 | // 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, and | 2819 | // 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 browser | 2827 | // Anything else follows the "Open links in" setting. The system browser |
| 2820 | // keeps the `_blank` the shell already handles; the in-app browser needs | 2828 | // 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 the | 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,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 | <PullRequestSummaryTab | 2338 | <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"; |
| 4 | import type { Options as ReactMarkdownOptions } from "react-markdown"; | 4 | import type { Options as ReactMarkdownOptions } from "react-markdown"; |
| 5 | 5 | ||
| 6 | import { cn } from "~/lib/utils"; | 6 | import { cn } from "~/lib/utils"; |
| 7 | import { PULL_REQUESTS_PANEL_REF } from "~/rightPanelStore"; | ||
| 7 | 8 | ||
| 8 | import ChatMarkdown from "../ChatMarkdown"; | 9 | import ChatMarkdown from "../ChatMarkdown"; |
| 9 | import { MediaVideoPlayer } from "../media/MediaVideoPlayer"; | 10 | import { MediaVideoPlayer } from "../media/MediaVideoPlayer"; |
| 10 | import { remarkPullRequestAutolinks, splitPullRequestBody } from "./pullRequestMarkdown.logic"; | 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 | /** 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. */ |
| 15 | export function PullRequestMarkdown({ | 19 | export 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( |
| 243 | 243 | ||
| 244 | export function useOpenChangeRequestLink( | 244 | export 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 the | 298 | // 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 | } |
| 318 | 339 |