authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-10-11 14:25:12-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-11 21:25:12+00:00
log81dbc870dad7b64d9920c7526862f52993266c93
treede93c9d0cbf4c0878ba9f81b328a7571dc3a5db4
parentd07a07f33c0a1b317dea6234636976231c3688b5
signature Signed by PGP key B5690EEEBB952194

fix(mobile): keep bot badges out of PR attachments (#18347)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

3 files changed, 85 insertions(+), 1 deletions(-)

apps/mobile/src/features/pull-requests/PullRequestDescription.tsx+6-1
...@@ -15,6 +15,7 @@ import { SymbolView } from "../../components/AppSymbol";...@@ -15,6 +15,7 @@ import { SymbolView } from "../../components/AppSymbol";
15import { AppText as Text } from "../../components/AppText";15import { AppText as Text } from "../../components/AppText";
16import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";16import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider";
17import type { FilePreviewItem } from "../../components/FilePreviewModal";17import type { FilePreviewItem } from "../../components/FilePreviewModal";
18import { isBotBadgeImage } from "./botBadgeImages";
18import { PullRequestMarkdown } from "./PullRequestMarkdown";19import { PullRequestMarkdown } from "./PullRequestMarkdown";
19import { pullRequestDescriptionContent } from "./pullRequestDescriptionContent";20import { pullRequestDescriptionContent } from "./pullRequestDescriptionContent";
2021
...@@ -35,7 +36,11 @@ export function PullRequestDescription(props: {...@@ -35,7 +36,11 @@ export function PullRequestDescription(props: {
35 parseMarkdownWithOptions(props.markdown, { gfm: true, html: true, math: false }),36 parseMarkdownWithOptions(props.markdown, { gfm: true, html: true, math: false }),
36 (markdown) => parseMarkdownWithOptions(markdown, { gfm: true, html: false, math: false }),37 (markdown) => parseMarkdownWithOptions(markdown, { gfm: true, html: false, math: false }),
37 );38 );
38 return pullRequestDescriptionContent(document, splitPullRequestBody(props.markdown));39 const content = pullRequestDescriptionContent(document, splitPullRequestBody(props.markdown));
40 return {
41 ...content,
42 attachments: content.attachments.filter((attachment) => !isBotBadgeImage(attachment.uri)),
43 };
39 }, [props.markdown]);44 }, [props.markdown]);
40 const gallery = useMemo<FilePreviewItem[]>(45 const gallery = useMemo<FilePreviewItem[]>(
41 () =>46 () =>
apps/mobile/src/features/pull-requests/botBadgeImages.test.ts created+26
...@@ -0,0 +1,26 @@
1import { describe, expect, it } from "vite-plus/test";
2import { isBotBadgeImage } from "./botBadgeImages";
3
4describe("bot badge images", () => {
5 it("matches bot badges but not screenshots hosted by the same services", () => {
6 const badges = [
7 "https://static.devin.ai/assets/gh-devin-review-light.svg?v=4",
8 "https://cursor.com/assets/images/open-in-web-dark.png",
9 "https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg",
10 "https://img.shields.io/badge/P1-orange",
11 "https://greptile-static-assets.s3.amazonaws.com/badges/ViewArtifacts.svg?v=4",
12 "https://www.gstatic.com/codereviewagent/high-priority.svg",
13 "https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png",
14 ];
15 const screenshots = [
16 // Cursor cloud agents embed their screenshots and recordings as artifacts.
17 "https://cursor.com/artifacts/c/art-41567e7e-97f5-46ff-9e6e-df6a85723409",
18 "https://www.cursor.com/artifacts/c/art-98360ed1-44f8-4459-bea3-43bf93cc74fc",
19 "https://app.devin.ai/attachments/abc/ss_1.png",
20 "https://user-images.githubusercontent.com/23264/1-other-upload.png",
21 "https://storage.googleapis.com/some-bucket/before.png",
22 ];
23 expect(badges.filter(isBotBadgeImage)).toEqual(badges);
24 expect(screenshots.filter(isBotBadgeImage)).toEqual([]);
25 });
26});
apps/mobile/src/features/pull-requests/botBadgeImages.ts created+53
...@@ -0,0 +1,53 @@
1/**
2 * Logos, buttons, and status icons that review bots, agents, and CI stamp into pull requests.
3 * Each entry is a host plus a path that only serves those assets, so screenshots uploaded to
4 * the same service (such as cursor.com/artifacts or app.devin.ai/attachments) stay visible.
5 * Only the description's Attachments strip skips them; markdown still renders them inline.
6 */
7const BOT_BADGE_PREFIXES = [
8 // Coding agents and review bots
9 "static.devin.ai/assets/",
10 "cursor.com/assets/images/",
11 "storage.googleapis.com/coderabbit_public_assets/",
12 "greptile-static-assets.s3.amazonaws.com/badges/",
13 "static.graphite.dev/",
14 "qodo.ai/wp-content/uploads/",
15 "cubic.dev/buttons/",
16 "github.githubassets.com/static/images/icons/copilot-code-review/",
17 "gstatic.com/codereviewagent/",
18 "vercel.com/vercel-agent-view-suggestion-",
19 "agents-vade-review.vercel.sh/",
20 "new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/",
21 "pr-comments-assets.blacksmith.sh/",
22 "app.roocode.com/cloud-agents/github/",
23 "app.parameter.ai/_static/icons/",
24 // CI, deploy, and dependency bots
25 "img.shields.io/",
26 "raster.shields.io/",
27 "dependabot-badges.githubapp.com/",
28 "developer.mend.io/api/mc/badges/",
29 "api.securityscorecards.dev/projects/",
30 "github-app-statics.socket.dev/",
31 "static.deepsource.com/comment_artifacts/",
32 "sonarsource.github.io/sonarcloud-github-static-resources/",
33 "codescene.io/imgs/svg/",
34 "app.codecov.io/gh/",
35 "github.com/codecov/engineering-team/assets/",
36 "codspeed.io/pr-report/",
37 "static.trunk.io/assets/",
38 "res.cloudinary.com/snyk/",
39 "api.segment.io/v1/pixel/",
40 "cla-assistant.io/pull/badge/",
41 "vercel.com/static/status/",
42 "vercel.com/api/www/avatar",
43 "app.netlify.com/qr-code/",
44 "workers.cloudflare.com/logo.svg",
45 // Cloudflare Pages reuses one old GitHub upload as its logo.
46 "user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png",
47 "qr.expo.dev/",
48];
49
50export function isBotBadgeImage(href: string) {
51 const path = href.replace(/^https?:\/\/(?:www\.)?/i, "").toLowerCase();
52 return BOT_BADGE_PREFIXES.some((prefix) => path.startsWith(prefix));
53}