From 45cca8a406b6d95ec2690dfe34fd98232821350c Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 5 Jun 2026 14:11:17 +0700 Subject: [PATCH] Fix iOS markdown image rendering --- packages/app/src/components/file-pane.tsx | 7 +++- .../src/components/markdown-text.android.tsx | 1 + .../app/src/components/markdown-text.ios.tsx | 19 +++++++--- .../app/src/components/markdown-text.web.tsx | 1 + packages/app/src/components/message.tsx | 7 +++- packages/app/src/utils/markdown-ast.test.ts | 35 +++++++++++++++++++ packages/app/src/utils/markdown-ast.ts | 12 +++++++ 7 files changed, 76 insertions(+), 6 deletions(-) create mode 100644 packages/app/src/utils/markdown-ast.test.ts create mode 100644 packages/app/src/utils/markdown-ast.ts diff --git a/packages/app/src/components/file-pane.tsx b/packages/app/src/components/file-pane.tsx index 6d70fdb4d..3617fd179 100644 --- a/packages/app/src/components/file-pane.tsx +++ b/packages/app/src/components/file-pane.tsx @@ -34,6 +34,7 @@ import { isRenderedMarkdownFile } from "@/components/file-pane-render-mode"; import { isWeb } from "@/constants/platform"; import { createMarkdownStyles } from "@/styles/markdown-styles"; import { getMarkdownListMarker, getMarkdownListSpacing } from "@/utils/markdown-list"; +import { markdownNodeContainsType } from "@/utils/markdown-ast"; import type { AttachmentMetadata } from "@/attachments/types"; import { useAttachmentPreviewUrl } from "@/attachments/use-attachment-preview-url"; import { persistAttachmentFromBytes } from "@/attachments/service"; @@ -395,7 +396,11 @@ function createFilePreviewMarkdownRules(): RenderRules { _parent: ASTNode[], styles: MarkdownStyles, ) => ( - + {children} ), diff --git a/packages/app/src/components/markdown-text.android.tsx b/packages/app/src/components/markdown-text.android.tsx index 7733afb82..dbaabd948 100644 --- a/packages/app/src/components/markdown-text.android.tsx +++ b/packages/app/src/components/markdown-text.android.tsx @@ -35,6 +35,7 @@ export function MarkdownTextSpan({ interface MarkdownParagraphViewProps { paragraphStyle: ViewStyle; + containsImage?: boolean; children: ReactNode; } diff --git a/packages/app/src/components/markdown-text.ios.tsx b/packages/app/src/components/markdown-text.ios.tsx index 8bd3b1046..8b925cebd 100644 --- a/packages/app/src/components/markdown-text.ios.tsx +++ b/packages/app/src/components/markdown-text.ios.tsx @@ -1,5 +1,5 @@ import { useMemo, type ReactNode } from "react"; -import type { StyleProp, TextProps, TextStyle, ViewStyle } from "react-native"; +import { View, type StyleProp, type TextProps, type TextStyle, type ViewStyle } from "react-native"; import { UITextView } from "react-native-uitextview"; interface MarkdownTextSpanProps { @@ -41,6 +41,7 @@ export function MarkdownTextSpan({ interface MarkdownParagraphViewProps { paragraphStyle: ViewStyle; + containsImage?: boolean; children: ReactNode; } @@ -52,13 +53,23 @@ const MARKDOWN_PARAGRAPH_RESET: ViewStyle = { marginBottom: 0 }; // ViewStyle is structurally compatible with the layout props paragraphs use // (margin, padding, alignment); the cast lets the existing paragraphStyle // flow through unchanged. -export function MarkdownParagraphView({ paragraphStyle, children }: MarkdownParagraphViewProps) { - const style = useMemo( +export function MarkdownParagraphView({ + paragraphStyle, + containsImage = false, + children, +}: MarkdownParagraphViewProps) { + const textStyle = useMemo( () => [paragraphStyle, MARKDOWN_PARAGRAPH_RESET] as StyleProp, [paragraphStyle], ); + const viewStyle = useMemo(() => [paragraphStyle, MARKDOWN_PARAGRAPH_RESET], [paragraphStyle]); + + if (containsImage) { + return {children}; + } + return ( - + {children} ); diff --git a/packages/app/src/components/markdown-text.web.tsx b/packages/app/src/components/markdown-text.web.tsx index 0d3a74753..0c9e4ceaa 100644 --- a/packages/app/src/components/markdown-text.web.tsx +++ b/packages/app/src/components/markdown-text.web.tsx @@ -46,6 +46,7 @@ export function MarkdownTextSpan({ interface MarkdownParagraphViewProps { paragraphStyle: ViewStyle; + containsImage?: boolean; children: ReactNode; } diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index f78a43808..ce2c738f2 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -70,6 +70,7 @@ import type { ToolCallDetail } from "@getpaseo/protocol/agent-types"; import { buildToolCallPresentation } from "@/tool-calls/presentation"; import { resolveToolCallIcon } from "@/utils/tool-call-icon"; import { getMarkdownListMarker, getMarkdownListSpacing } from "@/utils/markdown-list"; +import { markdownNodeContainsType } from "@/utils/markdown-ast"; import { useStableEvent } from "@/hooks/use-stable-event"; import { HighlightedCodeBlock } from "@/components/highlighted-code-block"; import { splitMarkdownBlocks } from "@/utils/split-markdown-blocks"; @@ -1825,7 +1826,11 @@ export const AssistantMessage = memo(function AssistantMessage({ _parent: ASTNode[], styles: MarkdownStyles, ) => ( - + {children} ), diff --git a/packages/app/src/utils/markdown-ast.test.ts b/packages/app/src/utils/markdown-ast.test.ts new file mode 100644 index 000000000..e7b439d79 --- /dev/null +++ b/packages/app/src/utils/markdown-ast.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vitest"; +import { markdownNodeContainsType } from "./markdown-ast"; + +describe("markdownNodeContainsType", () => { + it("matches the node itself", () => { + expect(markdownNodeContainsType({ type: "image", children: [] }, "image")).toBe(true); + }); + + it("matches descendants", () => { + const paragraph = { + type: "paragraph", + children: [ + { type: "text", children: [] }, + { + type: "link", + children: [{ type: "image", children: [] }], + }, + ], + }; + + expect(markdownNodeContainsType(paragraph, "image")).toBe(true); + }); + + it("returns false when the type is absent", () => { + const paragraph = { + type: "paragraph", + children: [ + { type: "text", children: [] }, + { type: "strong", children: [{ type: "text", children: [] }] }, + ], + }; + + expect(markdownNodeContainsType(paragraph, "image")).toBe(false); + }); +}); diff --git a/packages/app/src/utils/markdown-ast.ts b/packages/app/src/utils/markdown-ast.ts new file mode 100644 index 000000000..bcd2526e4 --- /dev/null +++ b/packages/app/src/utils/markdown-ast.ts @@ -0,0 +1,12 @@ +export interface MarkdownAstNodeWithChildren { + type: string; + children: MarkdownAstNodeWithChildren[]; +} + +export function markdownNodeContainsType(node: MarkdownAstNodeWithChildren, type: string): boolean { + if (node.type === type) { + return true; + } + + return node.children.some((child) => markdownNodeContainsType(child, type)); +}