diff --git a/packages/app/src/components/diff-viewer.tsx b/packages/app/src/components/diff-viewer.tsx index 0c2d24c56..a44e7808d 100644 --- a/packages/app/src/components/diff-viewer.tsx +++ b/packages/app/src/components/diff-viewer.tsx @@ -189,7 +189,7 @@ const styles = StyleSheet.create((theme) => { }, lineText: { fontFamily: Fonts.mono, - fontSize: theme.fontSize.xs, + fontSize: theme.fontSize.code, color: theme.colors.foreground, ...(isWeb ? { diff --git a/packages/app/src/components/file-explorer-pane.tsx b/packages/app/src/components/file-explorer-pane.tsx index ccff701c5..5bbf7bd46 100644 --- a/packages/app/src/components/file-explorer-pane.tsx +++ b/packages/app/src/components/file-explorer-pane.tsx @@ -1158,7 +1158,7 @@ const styles = StyleSheet.create((theme) => ({ codeText: { color: theme.colors.foreground, fontFamily: Fonts.mono, - fontSize: theme.fontSize.sm, + fontSize: theme.fontSize.code, flexShrink: 0, }, previewImageScrollContent: { diff --git a/packages/app/src/components/file-pane.tsx b/packages/app/src/components/file-pane.tsx index 02cb36c6a..0d3d69f2e 100644 --- a/packages/app/src/components/file-pane.tsx +++ b/packages/app/src/components/file-pane.tsx @@ -172,15 +172,15 @@ const codeLineStyles = StyleSheet.create((theme) => ({ gutterText: { color: theme.colors.foreground, fontFamily: Fonts.mono, - fontSize: theme.fontSize.sm, - lineHeight: theme.fontSize.sm * 1.45, + fontSize: theme.fontSize.code, + lineHeight: theme.fontSize.code * 1.45, opacity: 0.4, userSelect: "none", }, lineText: { fontFamily: Fonts.mono, - fontSize: theme.fontSize.sm, - lineHeight: theme.fontSize.sm * 1.45, + fontSize: theme.fontSize.code, + lineHeight: theme.fontSize.code * 1.45, flex: 1, }, })); @@ -216,9 +216,9 @@ function FilePreviewBody({ const gutterWidth = useMemo(() => { if (!highlightedLines) return 0; - return lineNumberGutterWidth(highlightedLines.length, theme.fontSize.sm); - }, [highlightedLines, theme.fontSize.sm]); - const lineHeight = theme.fontSize.sm * 1.45; + return lineNumberGutterWidth(highlightedLines.length, theme.fontSize.code); + }, [highlightedLines, theme.fontSize.code]); + const lineHeight = theme.fontSize.code * 1.45; const lineSelection = useMemo(() => { if (!highlightedLines) { return null; diff --git a/packages/app/src/components/highlighted-code-block.tsx b/packages/app/src/components/highlighted-code-block.tsx index 0822f884e..04a04bd2e 100644 --- a/packages/app/src/components/highlighted-code-block.tsx +++ b/packages/app/src/components/highlighted-code-block.tsx @@ -1,12 +1,5 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { - Pressable, - Text, - View, - type StyleProp, - type TextStyle, - type ViewStyle, -} from "react-native"; +import { Pressable, View, type StyleProp, type TextStyle, type ViewStyle } from "react-native"; import { StyleSheet } from "react-native-unistyles"; import { MarkdownTextSpan } from "@/components/markdown-text"; import * as Clipboard from "expo-clipboard"; @@ -47,6 +40,10 @@ function fenceLanguageToExtension(info: string | null | undefined): string | nul return LANGUAGE_ALIASES[normalized] ?? normalized; } +function stripTerminalFenceNewline(code: string): string { + return code.endsWith("\n") ? code.slice(0, -1) : code; +} + // Cross-instance cache for tokenized code blocks. Tokenization is // theme-independent (colors are applied at render time), so the key is just // (language, code). Bounded by entry count — 200 is generous for a chat @@ -88,23 +85,24 @@ export const HighlightedCodeBlock = React.memo(function HighlightedCodeBlock({ () => splitFenceStyle(inheritedStyles, textStyle), [inheritedStyles, textStyle], ); + const renderedCode = useMemo(() => stripTerminalFenceNewline(code), [code]); const keyedLines = useMemo(() => { const ext = fenceLanguageToExtension(language); if (!ext) return null; - const cacheKey = `${ext}:${code}`; + const cacheKey = `${ext}:${renderedCode}`; const cached = tokenizationCache.get(cacheKey); if (cached) return cached; let tokenizedLines: HighlightToken[][]; try { - tokenizedLines = highlightCode(code, `x.${ext}`); + tokenizedLines = highlightCode(renderedCode, `x.${ext}`); } catch { return null; } const result = tokenizedLines.map(toKeyedLine); tokenizationCache.set(cacheKey, result); return result; - }, [code, language]); + }, [renderedCode, language]); const isCompact = useIsCompactFormFactor(); const [isHovered, setIsHovered] = useState(false); @@ -120,18 +118,9 @@ export const HighlightedCodeBlock = React.memo(function HighlightedCodeBlock({ onPointerLeave={handlePointerLeave} > {keyedLines ? ( - - {keyedLines.map((line, lineIndex) => ( - - {lineIndex > 0 ? "\n" : null} - {line.tokens.map(({ key, token }) => ( - - ))} - - ))} - + {renderCodeSegments(keyedLines)} ) : ( - {code} + {renderedCode} )} @@ -158,13 +147,38 @@ function toKeyedLine(tokens: HighlightToken[], lineIndex: number): KeyedLine { }; } +function renderCodeSegments(keyedLines: KeyedLine[]): React.ReactNode[] { + const segments: React.ReactNode[] = []; + for (let lineIndex = 0; lineIndex < keyedLines.length; lineIndex += 1) { + const line = keyedLines[lineIndex]; + if (lineIndex > 0) { + segments.push(); + } + for (const { key, token } of line.tokens) { + segments.push(); + } + } + return segments; +} + interface TokenSpanProps { token: HighlightToken; } const TokenSpan = React.memo(function TokenSpan({ token }: TokenSpanProps) { - if (!token.style) return token.text; - return {token.text}; + return ( + + {token.text} + + ); +}); + +interface CodeTextSpanProps { + text: string; +} + +const CodeTextSpan = React.memo(function CodeTextSpan({ text }: CodeTextSpanProps) { + return {text}; }); interface SplitStyles { @@ -180,6 +194,7 @@ function splitFenceStyle(inheritedStyles: TextStyle, textStyle: TextStyle): Spli const textOnly: TextStyle = { ...WEB_SELECTABLE }; if (fontFamily !== undefined) textOnly.fontFamily = fontFamily; if (fontSize !== undefined) textOnly.fontSize = fontSize; + if (fontSize !== undefined) textOnly.lineHeight = Math.round(fontSize * 1.45); if (color !== undefined) textOnly.color = color; return { containerStyle: [box as ViewStyle, CONTAINER_BASE], diff --git a/packages/app/src/components/message.tsx b/packages/app/src/components/message.tsx index aa8cc3ca3..f4e73aa82 100644 --- a/packages/app/src/components/message.tsx +++ b/packages/app/src/components/message.tsx @@ -1975,7 +1975,7 @@ const activityLogStylesheet = StyleSheet.create((theme) => ({ }, metadataText: { color: theme.colors.foreground, - fontSize: theme.fontSize.xs, + fontSize: theme.fontSize.code, fontFamily: Fonts.mono, lineHeight: 16, }, diff --git a/packages/app/src/components/provider-diagnostic-sheet.tsx b/packages/app/src/components/provider-diagnostic-sheet.tsx index 53a4afa89..cac0a20dc 100644 --- a/packages/app/src/components/provider-diagnostic-sheet.tsx +++ b/packages/app/src/components/provider-diagnostic-sheet.tsx @@ -608,7 +608,7 @@ const sheetStyles = StyleSheet.create((theme) => ({ }, monoHint: { fontFamily: Fonts.mono, - fontSize: theme.fontSize.xs, + fontSize: theme.fontSize.code, color: theme.colors.foregroundMuted, flexShrink: 0, }, @@ -717,7 +717,7 @@ const sheetStyles = StyleSheet.create((theme) => ({ }, codeText: { fontFamily: Fonts.mono, - fontSize: theme.fontSize.xs, + fontSize: theme.fontSize.code, color: theme.colors.foreground, lineHeight: 18, }, diff --git a/packages/app/src/components/tool-call-details.tsx b/packages/app/src/components/tool-call-details.tsx index 40e35bddc..203232039 100644 --- a/packages/app/src/components/tool-call-details.tsx +++ b/packages/app/src/components/tool-call-details.tsx @@ -844,7 +844,7 @@ const styles = StyleSheet.create((theme) => { }, scrollText: { fontFamily: Fonts.mono, - fontSize: theme.fontSize.xs, + fontSize: theme.fontSize.code, color: theme.colors.foreground, lineHeight: 18, ...(isWeb @@ -859,7 +859,7 @@ const styles = StyleSheet.create((theme) => { }, subAgentSessionText: { fontFamily: Fonts.mono, - fontSize: theme.fontSize.xs, + fontSize: theme.fontSize.code, color: theme.colors.foregroundMuted, lineHeight: 18, marginBottom: theme.spacing[2], @@ -875,13 +875,13 @@ const styles = StyleSheet.create((theme) => { }, subAgentActionTool: { fontFamily: Fonts.mono, - fontSize: theme.fontSize.xs, + fontSize: theme.fontSize.code, color: theme.colors.foregroundMuted, lineHeight: 18, }, subAgentActionSummary: { fontFamily: Fonts.mono, - fontSize: theme.fontSize.xs, + fontSize: theme.fontSize.code, color: theme.colors.foreground, lineHeight: 18, }, diff --git a/packages/app/src/git/diff-pane.tsx b/packages/app/src/git/diff-pane.tsx index ed9de619e..7639442d2 100644 --- a/packages/app/src/git/diff-pane.tsx +++ b/packages/app/src/git/diff-pane.tsx @@ -944,7 +944,7 @@ function DiffFileBody({ hunk.newStart + hunk.newCount, ); } - const gutterWidth = lineNumberGutterWidth(maxLineNo, theme.fontSize.xs); + const gutterWidth = lineNumberGutterWidth(maxLineNo, theme.fontSize.code); if (layout === "split") { const rows = buildSplitDiffRows(file); @@ -2475,7 +2475,7 @@ const styles = StyleSheet.create((theme) => ({ width: "100%", textAlign: "right", paddingRight: theme.spacing[2], - fontSize: theme.fontSize.xs, + fontSize: theme.fontSize.code, lineHeight: theme.lineHeight.diff, fontFamily: Fonts.mono, color: theme.colors.foregroundMuted, @@ -2490,7 +2490,7 @@ const styles = StyleSheet.create((theme) => ({ diffLineText: { flex: 1, paddingRight: theme.spacing[3], - fontSize: theme.fontSize.xs, + fontSize: theme.fontSize.code, lineHeight: theme.lineHeight.diff, fontFamily: Fonts.mono, color: theme.colors.foreground, diff --git a/packages/app/src/panels/setup-panel.tsx b/packages/app/src/panels/setup-panel.tsx index 626b669d4..7bedb1100 100644 --- a/packages/app/src/panels/setup-panel.tsx +++ b/packages/app/src/panels/setup-panel.tsx @@ -541,7 +541,7 @@ const styles = StyleSheet.create((theme) => ({ }, logText: { fontFamily: Fonts.mono, - fontSize: theme.fontSize.sm, + fontSize: theme.fontSize.code, lineHeight: 20, color: theme.colors.foreground, }, diff --git a/packages/app/src/screens/startup-splash-screen.tsx b/packages/app/src/screens/startup-splash-screen.tsx index 625a9e6a7..d0d597f46 100644 --- a/packages/app/src/screens/startup-splash-screen.tsx +++ b/packages/app/src/screens/startup-splash-screen.tsx @@ -234,7 +234,7 @@ const styles = StyleSheet.create((theme) => ({ }, errorMessage: { color: theme.colors.destructive, - fontSize: theme.fontSize.sm, + fontSize: theme.fontSize.code, lineHeight: 20, fontFamily: Fonts.mono, }, @@ -258,7 +258,7 @@ const styles = StyleSheet.create((theme) => ({ }, logsText: { fontFamily: Fonts.mono, - fontSize: theme.fontSize.xs, + fontSize: theme.fontSize.code, color: theme.colors.foreground, lineHeight: 18, ...(isWeb diff --git a/packages/app/src/styles/markdown-styles.ts b/packages/app/src/styles/markdown-styles.ts index f99b033f3..7cea4afb9 100644 --- a/packages/app/src/styles/markdown-styles.ts +++ b/packages/app/src/styles/markdown-styles.ts @@ -170,7 +170,7 @@ export function createMarkdownStyles(theme: Theme) { borderRadius: theme.borderRadius.md, borderWidth: 0, fontFamily: Fonts.mono, - fontSize: theme.fontSize.sm, + fontSize: theme.fontSize.base - 3, }, code_block: { @@ -180,7 +180,7 @@ export function createMarkdownStyles(theme: Theme) { padding: theme.spacing[3], borderRadius: theme.borderRadius.md, fontFamily: Fonts.mono, - fontSize: theme.fontSize.sm, + fontSize: theme.fontSize.code, marginVertical: theme.spacing[2], }, @@ -193,7 +193,7 @@ export function createMarkdownStyles(theme: Theme) { borderWidth: 1, borderColor: theme.colors.border, fontFamily: Fonts.mono, - fontSize: theme.fontSize.sm, + fontSize: theme.fontSize.code, marginVertical: theme.spacing[3], }, @@ -388,18 +388,18 @@ export function createCompactMarkdownStyles(theme: Theme) { code_inline: { ...baseStyles.code_inline, - fontSize: theme.fontSize.xs, + fontSize: theme.fontSize.sm - 3, }, code_block: { ...baseStyles.code_block, - fontSize: theme.fontSize.xs, + fontSize: theme.fontSize.code, padding: theme.spacing[2], }, fence: { ...baseStyles.fence, - fontSize: theme.fontSize.xs, + fontSize: theme.fontSize.code, padding: theme.spacing[2], }, }; diff --git a/packages/app/src/styles/theme.ts b/packages/app/src/styles/theme.ts index ac2a9d112..680cc68d3 100644 --- a/packages/app/src/styles/theme.ts +++ b/packages/app/src/styles/theme.ts @@ -434,6 +434,7 @@ export const SPACING = { export const FONT_SIZE = { xs: 12, + code: 12, sm: 14, base: 16, lg: 18,