From 0613beb04d757d9c0f71d218d6de6ae2915a6175 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sun, 11 Oct 2026 14:22:08 -0700 Subject: [PATCH] fix(mobile): keep bot badges out of PR attachments The description's Attachments strip collected every embedded image, so review bot and agent banners (Devin Review, Cursor's Open in Web, CodeRabbit Change Stack, shields.io labels) showed up next to real screenshots. Skip images whose URL starts with a known bot-asset host and path. Each prefix includes a path, so screenshots on the same services stay, such as Cursor cloud agent artifacts and Devin attachments. Only the strip changes; the description and comments still render every image inline. 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 b11eeeb9954d..feb27ccf5d80 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 000000000000..eff5e0a43797 --- /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 000000000000..e533191b7920 --- /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)); +}