Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/components/chat/view/subcomponents/ChatComposer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ export default function ChatComposer({

<PromptInputTextarea
ref={textareaRef}
dir="auto"
value={input}
onChange={onInputChange}
onClick={onTextareaClick}
Expand Down
26 changes: 24 additions & 2 deletions src/components/chat/view/subcomponents/Markdown.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useMemo, useState } from 'react';
import { useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
Expand All @@ -21,9 +22,30 @@ type CodeBlockProps = {
children?: React.ReactNode;
};

const getCodeFontFamily = () => {
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');
Expand All @@ -34,6 +56,7 @@ const CodeBlock = ({ node, inline, className, children, ...props }: CodeBlockPro
<code
className={`whitespace-pre-wrap break-words rounded-md border border-gray-200 bg-gray-100 px-1.5 py-0.5 font-mono text-[0.9em] text-gray-900 dark:border-gray-700 dark:bg-gray-800/60 dark:text-gray-100 ${className || ''
}`}
style={{ fontFamily }}
{...props}
>
{children}
Expand Down Expand Up @@ -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,
},
}}
>
Expand Down
4 changes: 2 additions & 2 deletions src/components/chat/view/subcomponents/MessageComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
/* User message bubble on the right */
<div className="flex w-full items-end space-x-0 sm:w-auto sm:max-w-[85%] sm:space-x-3 md:max-w-md lg:max-w-lg xl:max-w-xl">
<div className="group flex-1 rounded-2xl rounded-br-md bg-blue-600 px-3 py-2 text-white shadow-sm sm:flex-initial sm:px-4">
<div className="whitespace-pre-wrap break-words text-sm">
<div dir="auto" className="whitespace-pre-wrap break-words text-sm">
{message.content}
</div>
{message.images && message.images.length > 0 && (
Expand Down Expand Up @@ -393,7 +393,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, o
</ReasoningContent>
</Reasoning>
) : (
<div className="text-sm text-gray-700 dark:text-gray-300">
<div dir="auto" className="text-sm text-gray-700 dark:text-gray-300">
{/* Reasoning accordion */}
{showThinking && message.reasoning && (
<Reasoning className="mb-3" defaultOpen={false}>
Expand Down
22 changes: 15 additions & 7 deletions src/components/settings/constants/constants.ts
Original file line number Diff line number Diff line change
@@ -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[] = [
Expand All @@ -27,11 +28,18 @@ 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 = {
allowedCommands: [],
disallowedCommands: [],
skipPermissions: false,
};

31 changes: 31 additions & 0 deletions src/components/settings/hooks/useSettingsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 <T>(response: Response): Promise<T> => response.json() as Promise<T>;
Expand Down Expand Up @@ -125,6 +135,9 @@ export function useSettingsController({ isOpen, initialTab }: UseSettingsControl
const [codeEditorSettings, setCodeEditorSettings] = useState<CodeEditorSettingsState>(() => (
readCodeEditorSettings()
));
const [appearanceFontSettings, setAppearanceFontSettings] = useState<AppearanceFontSettings>(() => (
readAppearanceFontSettings()
));

const [claudePermissions, setClaudePermissions] = useState<ClaudePermissionsState>(() => (
createEmptyClaudePermissions()
Expand Down Expand Up @@ -284,6 +297,13 @@ export function useSettingsController({ isOpen, initialTab }: UseSettingsControl
[],
);

const updateAppearanceFontSetting = useCallback(
<K extends keyof AppearanceFontSettings>(key: K, value: AppearanceFontSettings[K]) => {
setAppearanceFontSettings((prev) => ({ ...prev, [key]: value }));
},
[],
);

useEffect(() => {
if (!isOpen) {
return;
Expand All @@ -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<number | null>(null);
const isInitialLoadRef = useRef(true);
Expand Down Expand Up @@ -367,6 +396,8 @@ export function useSettingsController({ isOpen, initialTab }: UseSettingsControl
setProjectSortOrder,
codeEditorSettings,
updateCodeEditorSetting,
appearanceFontSettings,
updateAppearanceFontSetting,
claudePermissions,
setClaudePermissions,
cursorPermissions,
Expand Down
16 changes: 12 additions & 4 deletions src/components/settings/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

Expand Down
8 changes: 8 additions & 0 deletions src/components/settings/view/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ function Settings({ isOpen, onClose, projects = [], initialTab = 'agents' }: Set
setProjectSortOrder,
codeEditorSettings,
updateCodeEditorSetting,
appearanceFontSettings,
updateAppearanceFontSetting,
claudePermissions,
setClaudePermissions,
notificationPreferences,
Expand Down Expand Up @@ -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)}
/>
)}

Expand Down
92 changes: 91 additions & 1 deletion src/components/settings/view/tabs/AppearanceSettingsTab.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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({
Expand All @@ -27,6 +33,12 @@ export default function AppearanceSettingsTab({
onCodeEditorShowMinimapChange,
onCodeEditorLineNumbersChange,
onCodeEditorFontSizeChange,
onCodeEditorFontChange,
onCodeEditorCustomFontChange,
appearanceFontSettings,
onAppearanceFontChange,
onAppearanceCustomFontChange,
onAppearanceFontSizeChange,
}: AppearanceSettingsTabProps) {
const { t } = useTranslation('settings');

Expand All @@ -46,6 +58,55 @@ export default function AppearanceSettingsTab({
<SettingsSection title={t('mainTabs.appearance')}>
<SettingsCard>
<LanguageSelector />

<SettingsRow
label={t('appearanceSettings.font.label')}
description={t('appearanceSettings.font.description')}
>
<select
value={appearanceFontSettings.font}
onChange={(event) => onAppearanceFontChange(event.target.value)}
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 sm:w-36"
>
<option value="default">{t('appearanceSettings.font.default')}</option>
<option value="custom">{t('appearanceSettings.font.custom')}</option>
</select>
</SettingsRow>

{appearanceFontSettings.font === 'custom' && (
<SettingsRow
label={t('appearanceSettings.customFont.label')}
description={t('appearanceSettings.customFont.description')}
>
<input
type="text"
value={appearanceFontSettings.customFont}
onChange={(event) => 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"
/>
</SettingsRow>
)}

<SettingsRow
label={t('appearanceSettings.fontSize.label')}
description={t('appearanceSettings.fontSize.description')}
>
<select
value={appearanceFontSettings.fontSize}
onChange={(event) => onAppearanceFontSizeChange(event.target.value)}
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 sm:w-28"
>
<option value="12">12px</option>
<option value="13">13px</option>
<option value="14">14px</option>
<option value="15">15px</option>
<option value="16">16px</option>
<option value="17">17px</option>
<option value="18">18px</option>
<option value="20">20px</option>
</select>
</SettingsRow>
</SettingsCard>
</SettingsSection>

Expand Down Expand Up @@ -113,6 +174,35 @@ export default function AppearanceSettingsTab({
/>
</SettingsRow>

<SettingsRow
label={t('appearanceSettings.codeEditor.font.label')}
description={t('appearanceSettings.codeEditor.font.description')}
>
<select
value={codeEditorSettings.font}
onChange={(event) => onCodeEditorFontChange(event.target.value)}
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 sm:w-36"
>
<option value="default">{t('appearanceSettings.codeEditor.font.default')}</option>
<option value="custom">{t('appearanceSettings.codeEditor.font.custom')}</option>
</select>
</SettingsRow>

{codeEditorSettings.font === 'custom' && (
<SettingsRow
label={t('appearanceSettings.codeEditor.customFont.label')}
description={t('appearanceSettings.codeEditor.customFont.description')}
>
<input
type="text"
value={codeEditorSettings.customFont}
onChange={(event) => 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"
/>
</SettingsRow>
)}

<SettingsRow
label={t('appearanceSettings.codeEditor.fontSize.label')}
description={t('appearanceSettings.codeEditor.fontSize.description')}
Expand Down
Loading