Improve markdown styling and enable text selection on mobile

- Fix agent chat bottom scroll padding by adding overflow hidden to inputAreaContainer
- Create shared markdown-styles.ts with comprehensive styling for tables, headings, code blocks, blockquotes, and horizontal rules
- Enable text selection in AssistantMessage and AgentThoughtMessage by overriding markdown render rules with selectable={true}
- Path chips remain non-selectable to preserve onPress navigation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-12-23 12:32:52 +07:00
co-authored by Claude Opus 4.5
parent adc56da129
commit 38a4c70aec
4 changed files with 499 additions and 236 deletions
@@ -1139,6 +1139,7 @@ const styles = StyleSheet.create(((theme: any) => ({
minHeight: FOOTER_HEIGHT,
alignItems: "center",
width: "100%",
overflow: "hidden",
},
inputAreaContent: {
width: "100%",
@@ -42,6 +42,7 @@ import {
extractReadEntries,
} from "@/utils/tool-call-parsers";
import { ToolCallSheetProvider } from "./tool-call-sheet";
import { createMarkdownStyles } from "@/styles/markdown-styles";
const MAX_CHAT_WIDTH = 960;
type PermissionResolvedMessage = Extract<
@@ -594,62 +595,7 @@ function PermissionRequestCard({
);
const markdownStyles = useMemo(
() => ({
body: {
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
lineHeight: 22,
},
text: {
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
lineHeight: 22,
},
paragraph: {
marginBottom: theme.spacing[1],
},
strong: {
fontWeight: theme.fontWeight.semibold,
},
bullet_list: {
marginBottom: theme.spacing[1],
},
ordered_list: {
marginBottom: theme.spacing[1],
},
list_item: {
flexDirection: "row" as const,
marginBottom: theme.spacing[1],
},
code_inline: {
fontFamily: "monospace",
backgroundColor: theme.colors.card,
paddingHorizontal: theme.spacing[1],
paddingVertical: theme.spacing[1],
borderRadius: theme.borderRadius.sm,
},
code_block: {
fontFamily: "monospace",
backgroundColor: theme.colors.card,
padding: theme.spacing[2],
borderRadius: theme.borderRadius.md,
},
fence: {
fontFamily: "monospace",
backgroundColor: theme.colors.card,
padding: theme.spacing[2],
borderRadius: theme.borderRadius.md,
},
blockquote: {
borderLeftWidth: theme.borderWidth[1],
borderLeftColor: theme.colors.border,
paddingLeft: theme.spacing[3],
marginBottom: theme.spacing[2],
},
blockquote_text: {
color: theme.colors.mutedForeground,
},
}),
() => createMarkdownStyles(theme),
[theme]
);
+131 -180
View File
@@ -24,6 +24,7 @@ import {
} from "lucide-react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { baseColors, theme } from "@/styles/theme";
import { createMarkdownStyles, createCompactMarkdownStyles } from "@/styles/markdown-styles";
import { Colors } from "@/constants/theme";
import * as Clipboard from "expo-clipboard";
import type { TodoEntry, ThoughtStatus } from "@/types/stream";
@@ -161,27 +162,7 @@ export const assistantMessageStylesheet = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.xs,
fontWeight: theme.fontWeight.bold,
},
// Markdown styles
markdownBody: {
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
lineHeight: 24,
},
markdownText: {
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
lineHeight: 22,
},
markdownParagraph: {
marginTop: 0,
marginBottom: theme.spacing[2],
},
markdownStrong: {
fontWeight: theme.fontWeight.bold,
},
markdownEm: {
fontStyle: "italic" as const,
},
// Used in custom markdownRules for inline code styling
markdownCodeInline: {
backgroundColor: theme.colors.secondary,
color: theme.colors.secondaryForeground,
@@ -191,47 +172,7 @@ export const assistantMessageStylesheet = StyleSheet.create((theme) => ({
fontFamily: "monospace",
fontSize: 13,
},
markdownCodeBlock: {
backgroundColor: theme.colors.secondary,
color: theme.colors.secondaryForeground,
padding: theme.spacing[3],
borderRadius: theme.borderRadius.md,
fontFamily: "monospace",
fontSize: 13,
},
markdownFence: {
backgroundColor: theme.colors.secondary,
borderColor: theme.colors.border,
color: theme.colors.secondaryForeground,
padding: theme.spacing[3],
borderRadius: theme.borderRadius.md,
marginVertical: theme.spacing[2],
fontFamily: "monospace",
fontSize: 13,
},
markdownLink: {
color: theme.colors.primary,
textDecorationLine: "underline" as const,
},
markdownList: {
marginBottom: theme.spacing[2],
},
markdownListItem: {
marginBottom: theme.spacing[1],
},
markdownBlockquote: {
backgroundColor: theme.colors.secondary,
borderLeftWidth: 4,
borderLeftColor: theme.colors.primary,
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[2],
marginVertical: theme.spacing[2],
borderRadius: theme.borderRadius.sm,
},
markdownBlockquoteText: {
color: theme.colors.foreground,
fontStyle: "italic" as const,
},
// Used in custom markdownRules for path chip styling
pathChip: {
backgroundColor: theme.colors.secondary,
borderRadius: theme.borderRadius.full,
@@ -391,95 +332,75 @@ export const AssistantMessage = memo(function AssistantMessage({
const lastPathRef = useRef<string | null>(null);
const markdownStyles = useMemo(
() => ({
body: {
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
lineHeight: 24,
},
text: {
color: theme.colors.foreground,
},
paragraph: {
marginTop: 0,
marginBottom: theme.spacing[2],
},
strong: {
fontWeight: theme.fontWeight.bold,
},
em: {
fontStyle: "italic" as const,
},
code_inline: {
backgroundColor: theme.colors.secondary,
color: theme.colors.secondaryForeground,
paddingHorizontal: theme.spacing[2],
paddingVertical: 2,
borderRadius: theme.borderRadius.sm,
fontFamily: "monospace",
fontSize: 13,
},
code_block: {
backgroundColor: theme.colors.secondary,
color: theme.colors.secondaryForeground,
padding: theme.spacing[3],
borderRadius: theme.borderRadius.md,
fontFamily: "monospace",
fontSize: 13,
},
fence: {
backgroundColor: theme.colors.secondary,
borderColor: theme.colors.border,
color: theme.colors.secondaryForeground,
padding: theme.spacing[3],
borderRadius: theme.borderRadius.md,
marginVertical: theme.spacing[2],
fontFamily: "monospace",
fontSize: 13,
},
link: {
color: theme.colors.primary,
textDecorationLine: "underline" as const,
},
bullet_list: {
marginBottom: theme.spacing[2],
},
ordered_list: {
marginBottom: theme.spacing[2],
},
list_item: {
marginBottom: theme.spacing[1],
},
blockquote: {
backgroundColor: theme.colors.secondary,
borderLeftWidth: 4,
borderLeftColor: theme.colors.primary,
paddingHorizontal: theme.spacing[3],
paddingVertical: theme.spacing[2],
marginVertical: theme.spacing[2],
borderRadius: theme.borderRadius.sm,
},
blockquote_text: {
color: theme.colors.foreground,
fontStyle: "italic" as const,
},
}),
() => createMarkdownStyles(theme),
[theme]
);
const markdownRules = useMemo(() => {
if (!onInlinePathPress) {
return undefined;
}
return {
code_inline: (node: any) => {
text: (
node: any,
_children: ReactNode[],
_parent: any,
styles: any,
inheritedStyles: any = {}
) => (
<Text key={node.key} style={[inheritedStyles, styles.text]} selectable>
{node.content}
</Text>
),
textgroup: (
node: any,
children: ReactNode[],
_parent: any,
styles: any,
inheritedStyles: any = {}
) => (
<Text key={node.key} style={[inheritedStyles, styles.textgroup]} selectable>
{children}
</Text>
),
code_block: (
node: any,
_children: ReactNode[],
_parent: any,
styles: any,
inheritedStyles: any = {}
) => (
<Text key={node.key} style={[inheritedStyles, styles.code_block]} selectable>
{node.content}
</Text>
),
fence: (
node: any,
_children: ReactNode[],
_parent: any,
styles: any,
inheritedStyles: any = {}
) => (
<Text key={node.key} style={[inheritedStyles, styles.fence]} selectable>
{node.content}
</Text>
),
code_inline: (
node: any,
_children: ReactNode[],
_parent: any,
_styles: any,
inheritedStyles: any = {}
) => {
const content = node.content ?? "";
const parsed = parseInlinePathToken(content, lastPathRef);
const parsed = onInlinePathPress
? parseInlinePathToken(content, lastPathRef)
: null;
if (!parsed) {
return (
<Text key={node.key} style={assistantMessageStylesheet.markdownCodeInline}>
<Text
key={node.key}
style={[inheritedStyles, assistantMessageStylesheet.markdownCodeInline]}
selectable
>
{content}
</Text>
);
@@ -489,6 +410,7 @@ export const AssistantMessage = memo(function AssistantMessage({
<Text
key={node.key}
onPress={() => parsed && onInlinePathPress?.(parsed)}
selectable={false}
style={[assistantMessageStylesheet.pathChip, assistantMessageStylesheet.pathChipText]}
>
{content}
@@ -1020,42 +942,7 @@ export function AgentThoughtMessage({ message, status = "ready" }: AgentThoughtM
const [isExpanded, setIsExpanded] = useState(false);
const markdownContent = useMemo(() => message?.trim() ?? "", [message]);
const markdownStyles = useMemo(
() => ({
body: {
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
lineHeight: 20,
},
text: {
color: theme.colors.foreground,
},
paragraph: {
marginBottom: theme.spacing[2],
},
strong: {
fontWeight: theme.fontWeight.semibold,
},
em: {
fontStyle: "italic" as const,
},
code_inline: {
backgroundColor: theme.colors.secondary,
color: theme.colors.secondaryForeground,
paddingHorizontal: theme.spacing[1],
paddingVertical: 2,
borderRadius: theme.borderRadius.sm,
fontFamily: "monospace",
fontSize: theme.fontSize.xs,
},
code_block: {
backgroundColor: theme.colors.secondary,
color: theme.colors.secondaryForeground,
padding: theme.spacing[3],
borderRadius: theme.borderRadius.md,
fontFamily: "monospace",
fontSize: theme.fontSize.sm,
},
}),
() => createCompactMarkdownStyles(theme),
[theme]
);
@@ -1063,12 +950,76 @@ export function AgentThoughtMessage({ message, status = "ready" }: AgentThoughtM
setIsExpanded((prev) => !prev);
}, []);
const markdownRules = useMemo(() => {
return {
text: (
node: any,
_children: ReactNode[],
_parent: any,
styles: any,
inheritedStyles: any = {}
) => (
<Text key={node.key} style={[inheritedStyles, styles.text]} selectable>
{node.content}
</Text>
),
textgroup: (
node: any,
children: ReactNode[],
_parent: any,
styles: any,
inheritedStyles: any = {}
) => (
<Text key={node.key} style={[inheritedStyles, styles.textgroup]} selectable>
{children}
</Text>
),
code_block: (
node: any,
_children: ReactNode[],
_parent: any,
styles: any,
inheritedStyles: any = {}
) => (
<Text key={node.key} style={[inheritedStyles, styles.code_block]} selectable>
{node.content}
</Text>
),
fence: (
node: any,
_children: ReactNode[],
_parent: any,
styles: any,
inheritedStyles: any = {}
) => (
<Text key={node.key} style={[inheritedStyles, styles.fence]} selectable>
{node.content}
</Text>
),
code_inline: (
node: any,
_children: ReactNode[],
_parent: any,
styles: any,
inheritedStyles: any = {}
) => (
<Text key={node.key} style={[inheritedStyles, styles.code_inline]} selectable>
{node.content}
</Text>
),
};
}, []);
const renderDetails = useCallback(() => {
if (!markdownContent) {
return <Text style={agentThoughtStylesheet.emptyText}>No captured thinking</Text>;
}
return <Markdown style={markdownStyles}>{markdownContent}</Markdown>;
}, [markdownContent, markdownStyles]);
return (
<Markdown style={markdownStyles} rules={markdownRules}>
{markdownContent}
</Markdown>
);
}, [markdownContent, markdownRules, markdownStyles]);
return (
<ExpandableBadge
+365
View File
@@ -0,0 +1,365 @@
import type { Theme } from "./theme";
/**
* Creates comprehensive markdown styles for react-native-markdown-display.
*
* Usage:
* const markdownStyles = useMemo(() => createMarkdownStyles(theme), [theme]);
* <Markdown style={markdownStyles}>{content}</Markdown>
*/
export function createMarkdownStyles(theme: Theme) {
return {
// =========================================================================
// BASE STYLES
// =========================================================================
body: {
color: theme.colors.foreground,
fontSize: theme.fontSize.base,
lineHeight: 22,
},
text: {
color: theme.colors.foreground,
},
paragraph: {
marginTop: 0,
marginBottom: theme.spacing[3],
flexWrap: "wrap" as const,
flexDirection: "row" as const,
alignItems: "flex-start" as const,
justifyContent: "flex-start" as const,
},
// =========================================================================
// HEADINGS
// =========================================================================
heading1: {
fontSize: theme.fontSize["3xl"],
fontWeight: theme.fontWeight.bold,
color: theme.colors.foreground,
marginTop: theme.spacing[6],
marginBottom: theme.spacing[3],
lineHeight: 32,
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
paddingBottom: theme.spacing[2],
},
heading2: {
fontSize: theme.fontSize["2xl"],
fontWeight: theme.fontWeight.bold,
color: theme.colors.foreground,
marginTop: theme.spacing[6],
marginBottom: theme.spacing[3],
lineHeight: 28,
borderBottomWidth: 1,
borderBottomColor: theme.colors.border,
paddingBottom: theme.spacing[2],
},
heading3: {
fontSize: theme.fontSize.xl,
fontWeight: theme.fontWeight.semibold,
color: theme.colors.foreground,
marginTop: theme.spacing[4],
marginBottom: theme.spacing[2],
lineHeight: 26,
},
heading4: {
fontSize: theme.fontSize.lg,
fontWeight: theme.fontWeight.semibold,
color: theme.colors.foreground,
marginTop: theme.spacing[4],
marginBottom: theme.spacing[2],
lineHeight: 24,
},
heading5: {
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.semibold,
color: theme.colors.foreground,
marginTop: theme.spacing[3],
marginBottom: theme.spacing[1],
lineHeight: 22,
},
heading6: {
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.semibold,
color: theme.colors.mutedForeground,
marginTop: theme.spacing[3],
marginBottom: theme.spacing[1],
lineHeight: 20,
textTransform: "uppercase" as const,
letterSpacing: 0.5,
},
// =========================================================================
// TEXT FORMATTING
// =========================================================================
strong: {
fontWeight: theme.fontWeight.bold,
},
em: {
fontStyle: "italic" as const,
},
s: {
textDecorationLine: "line-through" as const,
color: theme.colors.mutedForeground,
},
link: {
color: theme.colors.primary,
textDecorationLine: "underline" as const,
},
blocklink: {
color: theme.colors.primary,
textDecorationLine: "underline" as const,
},
// =========================================================================
// CODE
// =========================================================================
code_inline: {
backgroundColor: theme.colors.secondary,
color: theme.colors.secondaryForeground,
paddingHorizontal: theme.spacing[1],
paddingVertical: 2,
borderRadius: theme.borderRadius.sm,
fontFamily: "monospace",
fontSize: theme.fontSize.sm,
},
code_block: {
backgroundColor: theme.colors.secondary,
color: theme.colors.secondaryForeground,
padding: theme.spacing[3],
borderRadius: theme.borderRadius.md,
fontFamily: "monospace",
fontSize: theme.fontSize.sm,
marginVertical: theme.spacing[2],
},
fence: {
backgroundColor: theme.colors.secondary,
color: theme.colors.secondaryForeground,
padding: theme.spacing[3],
borderRadius: theme.borderRadius.md,
borderWidth: 1,
borderColor: theme.colors.border,
fontFamily: "monospace",
fontSize: theme.fontSize.sm,
marginVertical: theme.spacing[3],
},
pre: {
marginVertical: theme.spacing[2],
},
// =========================================================================
// TABLES
// =========================================================================
table: {
borderWidth: 1,
borderColor: theme.colors.border,
borderRadius: theme.borderRadius.md,
marginVertical: theme.spacing[3],
},
thead: {
backgroundColor: theme.colors.secondary,
},
tbody: {},
th: {
padding: theme.spacing[2],
borderBottomWidth: 1,
borderRightWidth: 1,
borderColor: theme.colors.border,
backgroundColor: theme.colors.secondary,
fontWeight: theme.fontWeight.semibold,
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
textAlign: "left" as const,
},
tr: {
borderBottomWidth: 1,
borderColor: theme.colors.border,
flexDirection: "row" as const,
},
td: {
padding: theme.spacing[2],
borderRightWidth: 1,
borderColor: theme.colors.border,
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
flex: 1,
},
// =========================================================================
// LISTS
// =========================================================================
bullet_list: {
marginVertical: theme.spacing[2],
paddingLeft: theme.spacing[1],
},
ordered_list: {
marginVertical: theme.spacing[2],
paddingLeft: theme.spacing[1],
},
list_item: {
marginBottom: theme.spacing[1],
flexDirection: "row" as const,
alignItems: "flex-start" as const,
},
bullet_list_content: {
flex: 1,
flexWrap: "wrap" as const,
},
ordered_list_content: {
flex: 1,
flexWrap: "wrap" as const,
},
bullet_list_icon: {
color: theme.colors.mutedForeground,
marginRight: theme.spacing[2],
fontSize: theme.fontSize.base,
lineHeight: 22,
},
ordered_list_icon: {
color: theme.colors.mutedForeground,
marginRight: theme.spacing[2],
fontSize: theme.fontSize.base,
fontWeight: theme.fontWeight.semibold,
lineHeight: 22,
minWidth: 20,
},
// =========================================================================
// BLOCKQUOTE
// =========================================================================
blockquote: {
backgroundColor: theme.colors.muted,
borderLeftWidth: 4,
borderLeftColor: theme.colors.primary,
paddingHorizontal: theme.spacing[4],
paddingVertical: theme.spacing[3],
marginVertical: theme.spacing[3],
borderRadius: theme.borderRadius.md,
},
// =========================================================================
// HORIZONTAL RULE
// =========================================================================
hr: {
backgroundColor: theme.colors.border,
height: 1,
marginVertical: theme.spacing[6],
},
// =========================================================================
// IMAGES
// =========================================================================
image: {
borderRadius: theme.borderRadius.md,
marginVertical: theme.spacing[2],
},
// =========================================================================
// BREAKS
// =========================================================================
hardbreak: {
height: theme.spacing[2],
},
softbreak: {},
};
}
/**
* Creates a smaller variant of markdown styles for compact UI elements
* like thought bubbles, tooltips, or side panels.
*/
export function createCompactMarkdownStyles(theme: Theme) {
const baseStyles = createMarkdownStyles(theme);
return {
...baseStyles,
body: {
...baseStyles.body,
fontSize: theme.fontSize.sm,
lineHeight: 20,
},
heading1: {
...baseStyles.heading1,
fontSize: theme.fontSize.xl,
marginTop: theme.spacing[4],
marginBottom: theme.spacing[2],
lineHeight: 26,
},
heading2: {
...baseStyles.heading2,
fontSize: theme.fontSize.lg,
marginTop: theme.spacing[3],
marginBottom: theme.spacing[2],
lineHeight: 24,
},
heading3: {
...baseStyles.heading3,
fontSize: theme.fontSize.base,
marginTop: theme.spacing[3],
marginBottom: theme.spacing[1],
lineHeight: 22,
},
paragraph: {
...baseStyles.paragraph,
marginBottom: theme.spacing[2],
},
code_inline: {
...baseStyles.code_inline,
fontSize: theme.fontSize.xs,
},
code_block: {
...baseStyles.code_block,
fontSize: theme.fontSize.xs,
padding: theme.spacing[2],
},
fence: {
...baseStyles.fence,
fontSize: theme.fontSize.xs,
padding: theme.spacing[2],
},
};
}