diff --git a/src/components/chat/view/subcomponents/ChatComposer.tsx b/src/components/chat/view/subcomponents/ChatComposer.tsx index e67ee64bff..8847db8ebb 100644 --- a/src/components/chat/view/subcomponents/ChatComposer.tsx +++ b/src/components/chat/view/subcomponents/ChatComposer.tsx @@ -295,6 +295,7 @@ export default function ChatComposer({ { + const font = localStorage.getItem('codeEditorFont') || 'default'; + const customFont = localStorage.getItem('codeEditorCustomFont') || ''; + + if (font === 'custom' && customFont.trim()) { + return customFont; + } + return 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'; +}; + const CodeBlock = ({ node, inline, className, children, ...props }: CodeBlockProps) => { const { t } = useTranslation('chat'); const [copied, setCopied] = useState(false); + const [fontFamily, setFontFamily] = useState(getCodeFontFamily); + + useEffect(() => { + const handleFontChange = () => { + setFontFamily(getCodeFontFamily()); + }; + + window.addEventListener('codeEditorSettingsChanged', handleFontChange); + return () => window.removeEventListener('codeEditorSettingsChanged', handleFontChange); + }, []); + const raw = Array.isArray(children) ? children.join('') : String(children ?? ''); const looksMultiline = /[\r\n]/.test(raw); const inlineDetected = inline || (node && node.type === 'inlineCode'); @@ -34,6 +56,7 @@ const CodeBlock = ({ node, inline, className, children, ...props }: CodeBlockPro {children} @@ -105,8 +128,7 @@ const CodeBlock = ({ node, inline, className, children, ...props }: CodeBlockPro }} codeTagProps={{ style: { - fontFamily: - 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace', + fontFamily, }, }} > diff --git a/src/components/chat/view/subcomponents/MessageComponent.tsx b/src/components/chat/view/subcomponents/MessageComponent.tsx index 1b678a57aa..7cdf30dcb5 100644 --- a/src/components/chat/view/subcomponents/MessageComponent.tsx +++ b/src/components/chat/view/subcomponents/MessageComponent.tsx @@ -120,7 +120,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o /* User message bubble on the right */
-
+
{message.content}
{message.images && message.images.length > 0 && ( @@ -393,7 +393,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o ) : ( -
+
{/* Reasoning accordion */} {showThinking && message.reasoning && ( diff --git a/src/components/settings/constants/constants.ts b/src/components/settings/constants/constants.ts index 429aabbefe..98fb3e4dc4 100644 --- a/src/components/settings/constants/constants.ts +++ b/src/components/settings/constants/constants.ts @@ -1,10 +1,11 @@ import type { - AgentCategory, - AgentProvider, - CodeEditorSettingsState, - CursorPermissionsState, - ProjectSortOrder, - SettingsMainTab, + AgentCategory, + AgentProvider, + AppearanceFontSettings, + CodeEditorSettingsState, + CursorPermissionsState, + ProjectSortOrder, + SettingsMainTab, } from '../types/types'; export const SETTINGS_MAIN_TABS: SettingsMainTab[] = [ @@ -27,6 +28,14 @@ export const DEFAULT_CODE_EDITOR_SETTINGS: CodeEditorSettingsState = { showMinimap: true, lineNumbers: true, fontSize: '14', + font: 'default', + customFont: '', +}; + +export const DEFAULT_APPEARANCE_FONT_SETTINGS: AppearanceFontSettings = { + font: 'default', + customFont: '', + fontSize: '16', }; export const DEFAULT_CURSOR_PERMISSIONS: CursorPermissionsState = { @@ -34,4 +43,3 @@ export const DEFAULT_CURSOR_PERMISSIONS: CursorPermissionsState = { disallowedCommands: [], skipPermissions: false, }; - diff --git a/src/components/settings/hooks/useSettingsController.ts b/src/components/settings/hooks/useSettingsController.ts index 5463a5bbed..e854f4237c 100644 --- a/src/components/settings/hooks/useSettingsController.ts +++ b/src/components/settings/hooks/useSettingsController.ts @@ -3,11 +3,13 @@ import { useTheme } from '../../../contexts/ThemeContext'; import { authenticatedFetch } from '../../../utils/api'; import { useProviderAuthStatus } from '../../provider-auth/hooks/useProviderAuthStatus'; import { + DEFAULT_APPEARANCE_FONT_SETTINGS, DEFAULT_CODE_EDITOR_SETTINGS, DEFAULT_CURSOR_PERMISSIONS, } from '../constants/constants'; import type { AgentProvider, + AppearanceFontSettings, ClaudePermissionsState, CodeEditorSettingsState, CodexPermissionMode, @@ -89,6 +91,14 @@ const readCodeEditorSettings = (): CodeEditorSettingsState => ({ showMinimap: localStorage.getItem('codeEditorShowMinimap') !== 'false', lineNumbers: localStorage.getItem('codeEditorLineNumbers') !== 'false', fontSize: localStorage.getItem('codeEditorFontSize') ?? DEFAULT_CODE_EDITOR_SETTINGS.fontSize, + font: localStorage.getItem('codeEditorFont') ?? DEFAULT_CODE_EDITOR_SETTINGS.font, + customFont: localStorage.getItem('codeEditorCustomFont') ?? DEFAULT_CODE_EDITOR_SETTINGS.customFont, +}); + +const readAppearanceFontSettings = (): AppearanceFontSettings => ({ + font: localStorage.getItem('appearanceFont') ?? DEFAULT_APPEARANCE_FONT_SETTINGS.font, + customFont: localStorage.getItem('appearanceCustomFont') ?? DEFAULT_APPEARANCE_FONT_SETTINGS.customFont, + fontSize: localStorage.getItem('appearanceFontSize') ?? DEFAULT_APPEARANCE_FONT_SETTINGS.fontSize, }); const toResponseJson = async (response: Response): Promise => response.json() as Promise; @@ -125,6 +135,9 @@ export function useSettingsController({ isOpen, initialTab }: UseSettingsControl const [codeEditorSettings, setCodeEditorSettings] = useState(() => ( readCodeEditorSettings() )); + const [appearanceFontSettings, setAppearanceFontSettings] = useState(() => ( + readAppearanceFontSettings() + )); const [claudePermissions, setClaudePermissions] = useState(() => ( createEmptyClaudePermissions() @@ -284,6 +297,13 @@ export function useSettingsController({ isOpen, initialTab }: UseSettingsControl [], ); + const updateAppearanceFontSetting = useCallback( + (key: K, value: AppearanceFontSettings[K]) => { + setAppearanceFontSettings((prev) => ({ ...prev, [key]: value })); + }, + [], + ); + useEffect(() => { if (!isOpen) { return; @@ -300,9 +320,18 @@ export function useSettingsController({ isOpen, initialTab }: UseSettingsControl localStorage.setItem('codeEditorShowMinimap', String(codeEditorSettings.showMinimap)); localStorage.setItem('codeEditorLineNumbers', String(codeEditorSettings.lineNumbers)); localStorage.setItem('codeEditorFontSize', codeEditorSettings.fontSize); + localStorage.setItem('codeEditorFont', codeEditorSettings.font); + localStorage.setItem('codeEditorCustomFont', codeEditorSettings.customFont); window.dispatchEvent(new Event('codeEditorSettingsChanged')); }, [codeEditorSettings]); + useEffect(() => { + localStorage.setItem('appearanceFont', appearanceFontSettings.font); + localStorage.setItem('appearanceCustomFont', appearanceFontSettings.customFont); + localStorage.setItem('appearanceFontSize', appearanceFontSettings.fontSize); + window.dispatchEvent(new Event('appearanceFontSettingsChanged')); + }, [appearanceFontSettings]); + // Auto-save permissions and sort order with debounce const autoSaveTimerRef = useRef(null); const isInitialLoadRef = useRef(true); @@ -367,6 +396,8 @@ export function useSettingsController({ isOpen, initialTab }: UseSettingsControl setProjectSortOrder, codeEditorSettings, updateCodeEditorSetting, + appearanceFontSettings, + updateAppearanceFontSetting, claudePermissions, setClaudePermissions, cursorPermissions, diff --git a/src/components/settings/types/types.ts b/src/components/settings/types/types.ts index 8fe3b7ff35..fb93e845f9 100644 --- a/src/components/settings/types/types.ts +++ b/src/components/settings/types/types.ts @@ -44,10 +44,18 @@ export type CursorPermissionsState = { }; export type CodeEditorSettingsState = { - theme: 'dark' | 'light'; - wordWrap: boolean; - showMinimap: boolean; - lineNumbers: boolean; + theme: 'dark' | 'light'; + wordWrap: boolean; + showMinimap: boolean; + lineNumbers: boolean; + fontSize: string; + font: string; + customFont: string; +}; + +export type AppearanceFontSettings = { + font: string; + customFont: string; fontSize: string; }; diff --git a/src/components/settings/view/Settings.tsx b/src/components/settings/view/Settings.tsx index 8340a54752..afe1882b5b 100644 --- a/src/components/settings/view/Settings.tsx +++ b/src/components/settings/view/Settings.tsx @@ -25,6 +25,8 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set setProjectSortOrder, codeEditorSettings, updateCodeEditorSetting, + appearanceFontSettings, + updateAppearanceFontSetting, claudePermissions, setClaudePermissions, notificationPreferences, @@ -116,6 +118,12 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set onCodeEditorShowMinimapChange={(value) => updateCodeEditorSetting('showMinimap', value)} onCodeEditorLineNumbersChange={(value) => updateCodeEditorSetting('lineNumbers', value)} onCodeEditorFontSizeChange={(value) => updateCodeEditorSetting('fontSize', value)} + onCodeEditorFontChange={(value) => updateCodeEditorSetting('font', value)} + onCodeEditorCustomFontChange={(value) => updateCodeEditorSetting('customFont', value)} + appearanceFontSettings={appearanceFontSettings} + onAppearanceFontChange={(value) => updateAppearanceFontSetting('font', value)} + onAppearanceCustomFontChange={(value) => updateAppearanceFontSetting('customFont', value)} + onAppearanceFontSizeChange={(value) => updateAppearanceFontSetting('fontSize', value)} /> )} diff --git a/src/components/settings/view/tabs/AppearanceSettingsTab.tsx b/src/components/settings/view/tabs/AppearanceSettingsTab.tsx index b320ec5fce..3bedd6db03 100644 --- a/src/components/settings/view/tabs/AppearanceSettingsTab.tsx +++ b/src/components/settings/view/tabs/AppearanceSettingsTab.tsx @@ -1,6 +1,6 @@ import { useTranslation } from 'react-i18next'; import { DarkModeToggle } from '../../../../shared/view/ui'; -import type { CodeEditorSettingsState, ProjectSortOrder } from '../../types/types'; +import type { AppearanceFontSettings, CodeEditorSettingsState, ProjectSortOrder } from '../../types/types'; import LanguageSelector from '../../../../shared/view/ui/LanguageSelector'; import SettingsCard from '../SettingsCard'; import SettingsRow from '../SettingsRow'; @@ -16,6 +16,12 @@ type AppearanceSettingsTabProps = { onCodeEditorShowMinimapChange: (value: boolean) => void; onCodeEditorLineNumbersChange: (value: boolean) => void; onCodeEditorFontSizeChange: (value: string) => void; + onCodeEditorFontChange: (value: string) => void; + onCodeEditorCustomFontChange: (value: string) => void; + appearanceFontSettings: AppearanceFontSettings; + onAppearanceFontChange: (value: string) => void; + onAppearanceCustomFontChange: (value: string) => void; + onAppearanceFontSizeChange: (value: string) => void; }; export default function AppearanceSettingsTab({ @@ -27,6 +33,12 @@ export default function AppearanceSettingsTab({ onCodeEditorShowMinimapChange, onCodeEditorLineNumbersChange, onCodeEditorFontSizeChange, + onCodeEditorFontChange, + onCodeEditorCustomFontChange, + appearanceFontSettings, + onAppearanceFontChange, + onAppearanceCustomFontChange, + onAppearanceFontSizeChange, }: AppearanceSettingsTabProps) { const { t } = useTranslation('settings'); @@ -46,6 +58,55 @@ export default function AppearanceSettingsTab({ + + + + + + {appearanceFontSettings.font === 'custom' && ( + + onAppearanceCustomFontChange(event.target.value)} + placeholder={t('appearanceSettings.customFont.placeholder')} + className="w-full rounded-lg border border-input bg-card p-2.5 text-sm text-foreground touch-manipulation focus:border-primary focus:ring-1 focus:ring-primary" + /> + + )} + + + + @@ -113,6 +174,35 @@ export default function AppearanceSettingsTab({ /> + + + + + {codeEditorSettings.font === 'custom' && ( + + onCodeEditorCustomFontChange(event.target.value)} + placeholder={t('appearanceSettings.codeEditor.customFont.placeholder')} + className="w-full rounded-lg border border-input bg-card p-2.5 text-sm text-foreground touch-manipulation focus:border-primary focus:ring-1 focus:ring-primary" + /> + + )} + { diff --git a/src/utils/fontSettings.ts b/src/utils/fontSettings.ts new file mode 100644 index 0000000000..d404c7ae25 --- /dev/null +++ b/src/utils/fontSettings.ts @@ -0,0 +1,42 @@ +export const applyAppearanceFontSettings = () => { + const font = localStorage.getItem('appearanceFont') || 'default'; + const customFont = localStorage.getItem('appearanceCustomFont') || ''; + const fontSize = localStorage.getItem('appearanceFontSize') || '16'; + + const body = document.body; + + if (font === 'custom' && customFont.trim()) { + body.style.fontFamily = customFont; + } else { + body.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif'; + } + + body.style.fontSize = `${fontSize}px`; +}; + +export const applyCodeEditorFontSettings = () => { + const font = localStorage.getItem('codeEditorFont') || 'default'; + const customFont = localStorage.getItem('codeEditorCustomFont') || ''; + + const codeElements = document.querySelectorAll('code, pre code'); + + let fontFamily: string; + if (font === 'custom' && customFont.trim()) { + fontFamily = customFont; + } else { + fontFamily = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace'; + } + + codeElements.forEach((element) => { + (element as HTMLElement).style.fontFamily = fontFamily; + }); +}; + +export const initializeFontSettings = () => { + applyAppearanceFontSettings(); + applyCodeEditorFontSettings(); + + // Listen for settings changes + window.addEventListener('appearanceFontSettingsChanged', applyAppearanceFontSettings); + window.addEventListener('codeEditorSettingsChanged', applyCodeEditorFontSettings); +};