From 81dbc870dad7b64d9920c7526862f52993266c93 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 11 Oct 2026 14:25:12 -0700 Subject: [PATCH] fix(mobile): keep bot badges out of PR attachments (#18347) Co-authored-by: Claude Opus 5.5 --- .../pull-requests/PullRequestDescription.tsx | 7 ++- .../pull-requests/botBadgeImages.test.ts | 26 +++++++++ .../features/pull-requests/botBadgeImages.ts | 53 +++++++++++++++++++ 3 files changed, 85 insertions(+), 1 deletion(-) create mode 100644 apps/mobile/src/features/pull-requests/botBadgeImages.test.ts create mode 100644 apps/mobile/src/features/pull-requests/botBadgeImages.ts diff --git a/apps/mobile/src/features/pull-requests/PullRequestDescription.tsx b/apps/mobile/src/features/pull-requests/PullRequestDescription.tsx index b11eeeb9954d990862fca55b46f753a6be5a7a67..feb27ccf5d80db4f2a9e61666525445c82cbddc9 100644 --- a/apps/mobile/src/features/pull-requests/PullRequestDescription.tsx +++ b/apps/mobile/src/features/pull-requests/PullRequestDescription.tsx @@ -15,6 +15,7 @@ import { SymbolView } from "../../components/AppSymbol"; import { AppText as Text } from "../../components/AppText"; import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; import type { FilePreviewItem } from "../../components/FilePreviewModal"; +import { isBotBadgeImage } from "./botBadgeImages"; import { PullRequestMarkdown } from "./PullRequestMarkdown"; import { pullRequestDescriptionContent } from "./pullRequestDescriptionContent"; @@ -35,7 +36,11 @@ export function PullRequestDescription(props: { parseMarkdownWithOptions(props.markdown, { gfm: true, html: true, math: false }), (markdown) => parseMarkdownWithOptions(markdown, { gfm: true, html: false, math: false }), ); - return pullRequestDescriptionContent(document, splitPullRequestBody(props.markdown)); + const content = pullRequestDescriptionContent(document, splitPullRequestBody(props.markdown)); + return { + ...content, + attachments: content.attachments.filter((attachment) => !isBotBadgeImage(attachment.uri)), + }; }, [props.markdown]); const gallery = useMemo( () => diff --git a/apps/mobile/src/features/pull-requests/botBadgeImages.test.ts b/apps/mobile/src/features/pull-requests/botBadgeImages.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..eff5e0a4379722587c472fd1fb18035d8747c71d --- /dev/null +++ b/apps/mobile/src/features/pull-requests/botBadgeImages.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vite-plus/test"; +import { isBotBadgeImage } from "./botBadgeImages"; + +describe("bot badge images", () => { + it("matches bot badges but not screenshots hosted by the same services", () => { + const badges = [ + "https://static.devin.ai/assets/gh-devin-review-light.svg?v=4", + "https://cursor.com/assets/images/open-in-web-dark.png", + "https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg", + "https://img.shields.io/badge/P1-orange", + "https://greptile-static-assets.s3.amazonaws.com/badges/ViewArtifacts.svg?v=4", + "https://www.gstatic.com/codereviewagent/high-priority.svg", + "https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png", + ]; + const screenshots = [ + // Cursor cloud agents embed their screenshots and recordings as artifacts. + "https://cursor.com/artifacts/c/art-41567e7e-97f5-46ff-9e6e-df6a85723409", + "https://www.cursor.com/artifacts/c/art-98360ed1-44f8-4459-bea3-43bf93cc74fc", + "https://app.devin.ai/attachments/abc/ss_1.png", + "https://user-images.githubusercontent.com/23264/1-other-upload.png", + "https://storage.googleapis.com/some-bucket/before.png", + ]; + expect(badges.filter(isBotBadgeImage)).toEqual(badges); + expect(screenshots.filter(isBotBadgeImage)).toEqual([]); + }); +}); diff --git a/apps/mobile/src/features/pull-requests/botBadgeImages.ts b/apps/mobile/src/features/pull-requests/botBadgeImages.ts new file mode 100644 index 0000000000000000000000000000000000000000..e533191b79206efc2da243b3a50ab6617e8fac64 --- /dev/null +++ b/apps/mobile/src/features/pull-requests/botBadgeImages.ts @@ -0,0 +1,53 @@ +/** + * Logos, buttons, and status icons that review bots, agents, and CI stamp into pull requests. + * Each entry is a host plus a path that only serves those assets, so screenshots uploaded to + * the same service (such as cursor.com/artifacts or app.devin.ai/attachments) stay visible. + * Only the description's Attachments strip skips them; markdown still renders them inline. + */ +const BOT_BADGE_PREFIXES = [ + // Coding agents and review bots + "static.devin.ai/assets/", + "cursor.com/assets/images/", + "storage.googleapis.com/coderabbit_public_assets/", + "greptile-static-assets.s3.amazonaws.com/badges/", + "static.graphite.dev/", + "qodo.ai/wp-content/uploads/", + "cubic.dev/buttons/", + "github.githubassets.com/static/images/icons/copilot-code-review/", + "gstatic.com/codereviewagent/", + "vercel.com/vercel-agent-view-suggestion-", + "agents-vade-review.vercel.sh/", + "new-codeant-butcket.s3.us-west-1.amazonaws.com/badges/", + "pr-comments-assets.blacksmith.sh/", + "app.roocode.com/cloud-agents/github/", + "app.parameter.ai/_static/icons/", + // CI, deploy, and dependency bots + "img.shields.io/", + "raster.shields.io/", + "dependabot-badges.githubapp.com/", + "developer.mend.io/api/mc/badges/", + "api.securityscorecards.dev/projects/", + "github-app-statics.socket.dev/", + "static.deepsource.com/comment_artifacts/", + "sonarsource.github.io/sonarcloud-github-static-resources/", + "codescene.io/imgs/svg/", + "app.codecov.io/gh/", + "github.com/codecov/engineering-team/assets/", + "codspeed.io/pr-report/", + "static.trunk.io/assets/", + "res.cloudinary.com/snyk/", + "api.segment.io/v1/pixel/", + "cla-assistant.io/pull/badge/", + "vercel.com/static/status/", + "vercel.com/api/www/avatar", + "app.netlify.com/qr-code/", + "workers.cloudflare.com/logo.svg", + // Cloudflare Pages reuses one old GitHub upload as its logo. + "user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png", + "qr.expo.dev/", +]; + +export function isBotBadgeImage(href: string) { + const path = href.replace(/^https?:\/\/(?:www\.)?/i, "").toLowerCase(); + return BOT_BADGE_PREFIXES.some((prefix) => path.startsWith(prefix)); +} -- 2.54.0