Skip to content

Commit 9ecf588

Browse files
authored
Merge pull request #248 from pylon-code/upstream/2026-09-02-pull-requests
feat(pull-requests): link GitHub references, add list filters, defer line stats
2 parents 4564313 + 7c11248 commit 9ecf588

14 files changed

Lines changed: 1324 additions & 221 deletions

apps/web/src/components/ChatMarkdown.tsx

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,7 @@ interface ChatMarkdownProps {
180180
onUseArtifactTemplate?: ((template: CodexArtifactTemplate) => void) | undefined;
181181
imageBaseDir?: string | undefined;
182182
onImageExpand?: ((preview: ExpandedImagePreview) => void) | undefined;
183+
extraRemarkPlugins?: NonNullable<ReactMarkdownOptions["remarkPlugins"]>;
183184
}
184185

185186
export function canUseMarkdownFileShellActions(
@@ -229,6 +230,7 @@ export function shouldUseMarkdownFileBrowserPrimaryAction(input: {
229230
}
230231

231232
const EMPTY_MARKDOWN_SKILLS: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">> = [];
233+
const EMPTY_REMARK_PLUGINS: NonNullable<ReactMarkdownOptions["remarkPlugins"]> = [];
232234

233235
const ARTIFACT_TEMPLATE_ICON_BY_KIND = {
234236
document: FileTextIcon,
@@ -378,6 +380,7 @@ const CHAT_MARKDOWN_SANITIZE_SCHEMA = {
378380
code: [...(defaultSchema.attributes?.code ?? []), "dataCodeMeta", "dataInlineCode"],
379381
blockquote: [...(defaultSchema.attributes?.blockquote ?? []), "dataAlert"],
380382
div: [...(defaultSchema.attributes?.div ?? []), ...CODEX_ARTIFACT_TEMPLATE_HAST_PROPERTIES],
383+
a: [...(defaultSchema.attributes?.a ?? []), "dataPullRequestAutolink"],
381384
img: [...(defaultSchema.attributes?.img ?? []), "dataLocalSrc", "dataMarkdownTitle"],
382385
},
383386
protocols: {
@@ -1830,6 +1833,7 @@ function ChatMarkdown({
18301833
onUseArtifactTemplate,
18311834
imageBaseDir,
18321835
onImageExpand,
1836+
extraRemarkPlugins = EMPTY_REMARK_PLUGINS,
18331837
}: ChatMarkdownProps) {
18341838
const { resolvedTheme } = useTheme();
18351839
const createAssetUrl = useAtomQueryRunner(assetEnvironment.createUrl, {
@@ -2237,13 +2241,25 @@ function ChatMarkdown({
22372241
: null;
22382242
if (!fileLinkMeta) {
22392243
const faviconHost = resolveExternalWebLinkHost(href);
2244+
const pullRequestAutolink = String(
2245+
(props as Record<string, unknown>)["data-pull-request-autolink"] ?? "",
2246+
);
2247+
const pullRequestCopy =
2248+
pullRequestAutolink === "commit"
2249+
? /\/commit\/([0-9a-f]{40})$/iu.exec(href ?? "")?.[1]
2250+
: pullRequestAutolink === "reference"
2251+
? plainHastText(node)
2252+
: undefined;
2253+
const isPullRequestAutolink = pullRequestCopy !== undefined;
22402254
const isSameDocumentLink = href?.startsWith("#") ?? false;
22412255
const onClick = props.onClick;
22422256
const canOpenInPreview = Boolean(threadRef) && isPreviewSupportedInRuntime();
22432257
const linkChildren = <MarkdownLinkContext value>{children}</MarkdownLinkContext>;
22442258
const link = (
22452259
<a
22462260
{...props}
2261+
className={cn(props.className, pullRequestAutolink === "commit" && "font-mono")}
2262+
data-markdown-copy={pullRequestCopy}
22472263
href={href}
22482264
target={isSameDocumentLink ? undefined : "_blank"}
22492265
rel={isSameDocumentLink ? undefined : "noopener noreferrer"}
@@ -2315,7 +2331,7 @@ function ChatMarkdown({
23152331
});
23162332
}}
23172333
>
2318-
{faviconHost && hastHasText(node) ? (
2334+
{faviconHost && hastHasText(node) && !isPullRequestAutolink ? (
23192335
// The provider wraps the result rather than the children:
23202336
// MarkdownExternalLinkContent inspects its first child for the
23212337
// leading text it splits with <wbr/>, and an element there
@@ -2479,6 +2495,14 @@ function ChatMarkdown({
24792495
]);
24802496
/* eslint-enable react/no-unstable-nested-components */
24812497

2498+
const remarkPlugins = useMemo(
2499+
() => [
2500+
...(lineBreaks ? CHAT_MARKDOWN_REMARK_PLUGINS_WITH_BREAKS : CHAT_MARKDOWN_REMARK_PLUGINS),
2501+
...extraRemarkPlugins,
2502+
],
2503+
[extraRemarkPlugins, lineBreaks],
2504+
);
2505+
24822506
// react-markdown converts unparsed HTML nodes to text when skipHtml is false.
24832507
// Keep that behavior explicit because literal mode depends on escaping the
24842508
// complete source token instead of dropping it from the rendered message.
@@ -2491,9 +2515,7 @@ function ChatMarkdown({
24912515
onCopy={handleCopy}
24922516
>
24932517
<ReactMarkdown
2494-
remarkPlugins={
2495-
lineBreaks ? CHAT_MARKDOWN_REMARK_PLUGINS_WITH_BREAKS : CHAT_MARKDOWN_REMARK_PLUGINS
2496-
}
2518+
remarkPlugins={remarkPlugins}
24972519
rehypePlugins={parseRawHtml ? CHAT_MARKDOWN_REHYPE_PLUGINS : undefined}
24982520
skipHtml={false}
24992521
components={markdownComponents}

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ import { DiffPanelLoadingState } from "../DiffPanelShell";
9696
import { PullRequestsUnavailableState } from "./PullRequestsUnavailableState";
9797
import type { PullRequestAgentSelectionInput } from "./PullRequestCodeTab";
9898
import { openOnHostLabel, showPullRequestLinkContextMenu } from "./pullRequestLinkContextMenu";
99+
import { PullRequestMarkdownContext } from "./PullRequestMarkdown";
99100
import { PullRequestSummaryTab } from "./PullRequestSummaryTab";
100101
import { PullRequestTimelineTab } from "./PullRequestTimelineTab";
101102
import {
@@ -1921,7 +1922,7 @@ export function PullRequestDetailPanel({
19211922
{...(unavailableGitHubUrl ? { gitHubUrl: unavailableGitHubUrl } : {})}
19221923
/>
19231924
) : detail ? (
1924-
<>
1925+
<PullRequestMarkdownContext value={detail.provider === "github" ? repositoryUrl : null}>
19251926
{mountedTabs.has("summary") ? (
19261927
<div className={cn("absolute inset-0", tab !== "summary" && "invisible")}>
19271928
<PullRequestSummaryTab
@@ -1978,7 +1979,7 @@ export function PullRequestDetailPanel({
19781979
</Suspense>
19791980
</div>
19801981
) : null}
1981-
</>
1982+
</PullRequestMarkdownContext>
19821983
) : null}
19831984
</div>
19841985

apps/web/src/components/pullRequest/PullRequestListFilters.test.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ function findLabeledGroup(node: ReactNode, label: string): ReactNode {
3434
if (!isValidElement(child)) continue;
3535
const props = child.props as { readonly children?: ReactNode; readonly label?: string };
3636
if (props.label === label && typeof child.type === "function") {
37-
return (child.type as (properties: unknown) => ReactNode)(child.props);
37+
const rendered = (child.type as (properties: unknown) => ReactNode)(child.props);
38+
return findLabeledGroup(rendered, label) ?? rendered;
3839
}
3940
const nested = findLabeledGroup(props.children, label);
4041
if (nested !== undefined) return nested;
@@ -126,7 +127,7 @@ describe("pull request filters menu", () => {
126127
projectEnvironmentId: environmentId,
127128
onProject,
128129
});
129-
const radioGroup = findValueChange(view);
130+
const radioGroup = findValueChange(findLabeledGroup(view, "Project"));
130131
expect(radioGroup).toBeDefined();
131132

132133
radioGroup?.props.onValueChange(pullRequestProjectKey({ id: projectId, environmentId }));
@@ -156,7 +157,7 @@ describe("pull request filters menu", () => {
156157
],
157158
onProject,
158159
});
159-
const radioGroup = findValueChange(view);
160+
const radioGroup = findValueChange(findLabeledGroup(view, "Project"));
160161
expect(radioGroup).toBeDefined();
161162

162163
radioGroup?.props.onValueChange(

0 commit comments

Comments
 (0)