Skip to content

Commit bcbb3b1

Browse files
author
Test
committed
feat: render markdown in mobile reasoning
Change-Id: I5cc34c9c14fa379086608d9229c32acb65f85a1e Signed-off-by: Test <test@example.com>
1 parent 011472c commit bcbb3b1

File tree

5 files changed

+268
-154
lines changed

5 files changed

+268
-154
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { JSX } from "react";
2+
import { useMemo } from "react";
3+
import Markdown from "react-native-markdown-display";
4+
import type { MarkdownStyle } from "react-native-markdown-display";
5+
import { useTheme } from "../theme";
6+
import { assert } from "../utils/assert";
7+
import { createMarkdownStyles, type MarkdownVariant } from "../messages/markdownStyles";
8+
import { normalizeMarkdown } from "../messages/markdownUtils";
9+
10+
export interface MarkdownMessageBodyProps {
11+
content: string | null | undefined;
12+
variant: MarkdownVariant;
13+
styleOverrides?: Partial<MarkdownStyle>;
14+
}
15+
16+
export function MarkdownMessageBody({ content, variant, styleOverrides }: MarkdownMessageBodyProps): JSX.Element | null {
17+
assert(
18+
content === undefined || content === null || typeof content === "string",
19+
"MarkdownMessageBody expects string content",
20+
);
21+
22+
const theme = useTheme();
23+
24+
const normalizedContent = useMemo(() => {
25+
if (typeof content !== "string") {
26+
return "";
27+
}
28+
29+
return normalizeMarkdown(content);
30+
}, [content]);
31+
32+
const trimmed = normalizedContent.trim();
33+
if (trimmed.length === 0) {
34+
return null;
35+
}
36+
37+
const markdownStyles = useMemo(() => {
38+
const base = createMarkdownStyles(theme, variant);
39+
if (!styleOverrides) {
40+
return base;
41+
}
42+
43+
return {
44+
...base,
45+
...styleOverrides,
46+
} satisfies MarkdownStyle;
47+
}, [theme, variant, styleOverrides]);
48+
49+
return <Markdown style={markdownStyles}>{normalizedContent}</Markdown>;
50+
}

apps/mobile/src/components/ProposePlanCard.tsx

Lines changed: 66 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import type { JSX } from "react";
2-
import { useState } from "react";
2+
import { useMemo, useState } from "react";
33
import { Pressable, ScrollView, Text, View } from "react-native";
44
import * as Clipboard from "expo-clipboard";
5-
import Markdown from "react-native-markdown-display";
65
import { Surface } from "./Surface";
76
import { ThemedText } from "./ThemedText";
87
import { useTheme } from "../theme";
98
import { StartHereModal } from "./StartHereModal";
9+
import { MarkdownMessageBody } from "./MarkdownMessageBody";
1010

1111
interface ProposePlanCardProps {
1212
title: string;
@@ -26,6 +26,69 @@ export function ProposePlanCard({
2626
const theme = useTheme();
2727
const spacing = theme.spacing;
2828
const [showRaw, setShowRaw] = useState(false);
29+
const markdownOverrides = useMemo(
30+
() => ({
31+
body: {
32+
color: theme.colors.foregroundPrimary,
33+
fontSize: 14,
34+
lineHeight: 20,
35+
},
36+
heading1: {
37+
color: theme.colors.planModeLight,
38+
fontSize: 18,
39+
fontWeight: "bold",
40+
marginTop: spacing.sm,
41+
marginBottom: spacing.xs,
42+
},
43+
heading2: {
44+
color: theme.colors.planModeLight,
45+
fontSize: 16,
46+
fontWeight: "600",
47+
marginTop: spacing.sm,
48+
marginBottom: spacing.xs,
49+
},
50+
heading3: {
51+
color: theme.colors.foregroundPrimary,
52+
fontSize: 14,
53+
fontWeight: "600",
54+
marginTop: spacing.xs,
55+
marginBottom: spacing.xs,
56+
},
57+
paragraph: {
58+
marginTop: 0,
59+
marginBottom: spacing.sm,
60+
},
61+
code_inline: {
62+
backgroundColor: "rgba(31, 107, 184, 0.15)",
63+
color: theme.colors.planModeLight,
64+
fontSize: 12,
65+
paddingHorizontal: 4,
66+
paddingVertical: 2,
67+
borderRadius: 3,
68+
fontFamily: theme.typography.familyMono,
69+
},
70+
code_block: {
71+
backgroundColor: theme.colors.background,
72+
borderRadius: theme.radii.sm,
73+
padding: spacing.sm,
74+
fontFamily: theme.typography.familyMono,
75+
fontSize: 12,
76+
},
77+
fence: {
78+
backgroundColor: theme.colors.background,
79+
borderRadius: theme.radii.sm,
80+
padding: spacing.sm,
81+
marginVertical: spacing.xs,
82+
},
83+
bullet_list: {
84+
marginVertical: spacing.xs,
85+
},
86+
ordered_list: {
87+
marginVertical: spacing.xs,
88+
},
89+
}),
90+
[spacing, theme]
91+
);
2992
const [copied, setCopied] = useState(false);
3093
const [showModal, setShowModal] = useState(false);
3194
const [isStartingHere, setIsStartingHere] = useState(false);
@@ -154,70 +217,7 @@ export function ProposePlanCard({
154217
</ScrollView>
155218
) : (
156219
<ScrollView showsVerticalScrollIndicator>
157-
<Markdown
158-
style={{
159-
body: {
160-
color: theme.colors.foregroundPrimary,
161-
fontSize: 14,
162-
lineHeight: 20,
163-
},
164-
heading1: {
165-
color: theme.colors.planModeLight,
166-
fontSize: 18,
167-
fontWeight: "bold",
168-
marginTop: spacing.sm,
169-
marginBottom: spacing.xs,
170-
},
171-
heading2: {
172-
color: theme.colors.planModeLight,
173-
fontSize: 16,
174-
fontWeight: "600",
175-
marginTop: spacing.sm,
176-
marginBottom: spacing.xs,
177-
},
178-
heading3: {
179-
color: theme.colors.foregroundPrimary,
180-
fontSize: 14,
181-
fontWeight: "600",
182-
marginTop: spacing.xs,
183-
marginBottom: spacing.xs,
184-
},
185-
paragraph: {
186-
marginTop: 0,
187-
marginBottom: spacing.sm,
188-
},
189-
code_inline: {
190-
backgroundColor: "rgba(31, 107, 184, 0.15)",
191-
color: theme.colors.planModeLight,
192-
fontSize: 12,
193-
paddingHorizontal: 4,
194-
paddingVertical: 2,
195-
borderRadius: 3,
196-
fontFamily: theme.typography.familyMono,
197-
},
198-
code_block: {
199-
backgroundColor: theme.colors.background,
200-
borderRadius: theme.radii.sm,
201-
padding: spacing.sm,
202-
fontFamily: theme.typography.familyMono,
203-
fontSize: 12,
204-
},
205-
fence: {
206-
backgroundColor: theme.colors.background,
207-
borderRadius: theme.radii.sm,
208-
padding: spacing.sm,
209-
marginVertical: spacing.xs,
210-
},
211-
bullet_list: {
212-
marginVertical: spacing.xs,
213-
},
214-
ordered_list: {
215-
marginVertical: spacing.xs,
216-
},
217-
}}
218-
>
219-
{plan}
220-
</Markdown>
220+
<MarkdownMessageBody variant="plan" content={plan} styleOverrides={markdownOverrides} />
221221
</ScrollView>
222222
)}
223223
</View>

apps/mobile/src/messages/MessageRenderer.tsx

Lines changed: 13 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import type { JSX } from "react";
2-
import Markdown from "react-native-markdown-display";
32
import {
43
Image,
54
View,
@@ -15,6 +14,8 @@ import {
1514
Keyboard,
1615
} from "react-native";
1716
import { useMemo, useState, useEffect, useRef } from "react";
17+
import { MarkdownMessageBody } from "../components/MarkdownMessageBody";
18+
import { hasRenderableMarkdown } from "./markdownUtils";
1819
import { Ionicons } from "@expo/vector-icons";
1920
import { Surface } from "../components/Surface";
2021
import { ThemedText } from "../components/ThemedText";
@@ -146,87 +147,6 @@ function AssistantMessageCard({
146147
await Clipboard.setStringAsync(message.content);
147148
};
148149

149-
const markdownStyles = useMemo(
150-
() => ({
151-
body: {
152-
color: theme.colors.foregroundPrimary,
153-
fontFamily: theme.typography.familyPrimary,
154-
fontSize: theme.typography.sizes.body,
155-
lineHeight: theme.typography.lineHeights.normal,
156-
},
157-
code_block: {
158-
backgroundColor: theme.colors.surfaceSunken,
159-
borderRadius: theme.radii.sm,
160-
borderWidth: StyleSheet.hairlineWidth,
161-
borderColor: theme.colors.separator,
162-
padding: theme.spacing.sm,
163-
fontFamily: theme.typography.familyMono,
164-
fontSize: theme.typography.sizes.caption,
165-
color: theme.colors.foregroundPrimary,
166-
},
167-
code_inline: {
168-
fontFamily: theme.typography.familyMono,
169-
backgroundColor: theme.colors.surfaceSunken,
170-
paddingHorizontal: theme.spacing.xs,
171-
paddingVertical: 1,
172-
borderRadius: theme.radii.xs,
173-
color: theme.colors.foregroundPrimary,
174-
fontSize: theme.typography.sizes.caption,
175-
},
176-
fence: {
177-
backgroundColor: theme.colors.surfaceSunken,
178-
borderRadius: theme.radii.sm,
179-
borderWidth: StyleSheet.hairlineWidth,
180-
borderColor: theme.colors.separator,
181-
padding: theme.spacing.sm,
182-
marginVertical: theme.spacing.xs,
183-
},
184-
pre: {
185-
backgroundColor: theme.colors.surfaceSunken,
186-
borderRadius: theme.radii.sm,
187-
padding: theme.spacing.sm,
188-
fontFamily: theme.typography.familyMono,
189-
fontSize: theme.typography.sizes.caption,
190-
color: theme.colors.foregroundPrimary,
191-
},
192-
text: {
193-
fontFamily: theme.typography.familyMono,
194-
fontSize: theme.typography.sizes.caption,
195-
color: theme.colors.foregroundPrimary,
196-
},
197-
bullet_list: {
198-
marginVertical: theme.spacing.xs,
199-
},
200-
ordered_list: {
201-
marginVertical: theme.spacing.xs,
202-
},
203-
blockquote: {
204-
borderLeftColor: theme.colors.accent,
205-
borderLeftWidth: 2,
206-
paddingLeft: theme.spacing.md,
207-
color: theme.colors.foregroundSecondary,
208-
},
209-
heading1: {
210-
color: theme.colors.foregroundPrimary,
211-
fontSize: theme.typography.sizes.titleLarge,
212-
fontWeight: theme.typography.weights.bold,
213-
marginVertical: theme.spacing.sm,
214-
},
215-
heading2: {
216-
color: theme.colors.foregroundPrimary,
217-
fontSize: theme.typography.sizes.titleMedium,
218-
fontWeight: theme.typography.weights.semibold,
219-
marginVertical: theme.spacing.sm,
220-
},
221-
heading3: {
222-
color: theme.colors.foregroundPrimary,
223-
fontSize: theme.typography.sizes.titleSmall,
224-
fontWeight: theme.typography.weights.semibold,
225-
marginVertical: theme.spacing.xs,
226-
},
227-
}),
228-
[theme]
229-
);
230150

231151
return (
232152
<Pressable onPress={handlePress} onLongPress={handleLongPress} delayLongPress={500}>
@@ -287,7 +207,7 @@ function AssistantMessageCard({
287207
<View style={{ flexDirection: "row", alignItems: "flex-end" }}>
288208
<View style={{ flex: 1 }}>
289209
{Boolean(message.content) ? (
290-
<Markdown style={markdownStyles}>{message.content}</Markdown>
210+
<MarkdownMessageBody variant="assistant" content={message.content} />
291211
) : (
292212
<ThemedText variant="muted">(No content)</ThemedText>
293213
)}
@@ -506,6 +426,7 @@ function ReasoningMessageCard({
506426
const theme = useTheme();
507427
const isStreaming = "isStreaming" in message && (message as any).isStreaming === true;
508428
const [isExpanded, setIsExpanded] = useState(true); // Default expanded
429+
const hasReasoningContent = hasRenderableMarkdown(message.content);
509430

510431
// Auto-collapse when reasoning finishes (isStreaming becomes false)
511432
useEffect(() => {
@@ -532,11 +453,15 @@ function ReasoningMessageCard({
532453

533454
{isExpanded && (
534455
<View style={{ flexDirection: "row", alignItems: "flex-end", marginTop: theme.spacing.sm }}>
535-
<ThemedText
536-
style={{ flex: 1, fontStyle: "italic", color: theme.colors.foregroundSecondary }}
537-
>
538-
{message.content || "(Thinking…)"}
539-
</ThemedText>
456+
<View style={{ flex: 1 }}>
457+
{hasReasoningContent ? (
458+
<MarkdownMessageBody variant="reasoning" content={message.content} />
459+
) : (
460+
<ThemedText style={{ fontStyle: "italic", color: theme.colors.foregroundSecondary }}>
461+
{"(Thinking…)"}
462+
</ThemedText>
463+
)}
464+
</View>
540465
{isStreaming && <StreamingCursor />}
541466
</View>
542467
)}

0 commit comments

Comments
 (0)