diff --git a/packages/app/src/components/agent-input-area.tsx b/packages/app/src/components/agent-input-area.tsx index f8f9398bd..2c6651e32 100644 --- a/packages/app/src/components/agent-input-area.tsx +++ b/packages/app/src/components/agent-input-area.tsx @@ -1139,6 +1139,7 @@ const styles = StyleSheet.create(((theme: any) => ({ minHeight: FOOTER_HEIGHT, alignItems: "center", width: "100%", + overflow: "hidden", }, inputAreaContent: { width: "100%", diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index 55a0a5cba..3ace1f31d 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -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] ); diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index 4b464adc6..19f95df09 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -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(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 = {} + ) => ( + + {node.content} + + ), + textgroup: ( + node: any, + children: ReactNode[], + _parent: any, + styles: any, + inheritedStyles: any = {} + ) => ( + + {children} + + ), + code_block: ( + node: any, + _children: ReactNode[], + _parent: any, + styles: any, + inheritedStyles: any = {} + ) => ( + + {node.content} + + ), + fence: ( + node: any, + _children: ReactNode[], + _parent: any, + styles: any, + inheritedStyles: any = {} + ) => ( + + {node.content} + + ), + 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 ( - + {content} ); @@ -489,6 +410,7 @@ export const AssistantMessage = memo(function AssistantMessage({ 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 = {} + ) => ( + + {node.content} + + ), + textgroup: ( + node: any, + children: ReactNode[], + _parent: any, + styles: any, + inheritedStyles: any = {} + ) => ( + + {children} + + ), + code_block: ( + node: any, + _children: ReactNode[], + _parent: any, + styles: any, + inheritedStyles: any = {} + ) => ( + + {node.content} + + ), + fence: ( + node: any, + _children: ReactNode[], + _parent: any, + styles: any, + inheritedStyles: any = {} + ) => ( + + {node.content} + + ), + code_inline: ( + node: any, + _children: ReactNode[], + _parent: any, + styles: any, + inheritedStyles: any = {} + ) => ( + + {node.content} + + ), + }; + }, []); + const renderDetails = useCallback(() => { if (!markdownContent) { return No captured thinking; } - return {markdownContent}; - }, [markdownContent, markdownStyles]); + return ( + + {markdownContent} + + ); + }, [markdownContent, markdownRules, markdownStyles]); return ( createMarkdownStyles(theme), [theme]); + * {content} + */ +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], + }, + }; +}