Feat: spell check toggle for desktop (#626)
* feat: (desktop) add spellcheck toggle for text inputs * fix: show spellcheck toggle only on desktop * fix: apply the preference consistently to chat and commit inputs
This commit is contained in:
@@ -155,7 +155,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
const { currentProviderId, currentModelId, currentVariant, currentAgentName, setAgent, getVisibleAgents } = useConfigStore();
|
const { currentProviderId, currentModelId, currentVariant, currentAgentName, setAgent, getVisibleAgents } = useConfigStore();
|
||||||
const agents = getVisibleAgents();
|
const agents = getVisibleAgents();
|
||||||
const primaryAgents = React.useMemo(() => agents.filter((agent) => agent.mode === 'primary'), [agents]);
|
const primaryAgents = React.useMemo(() => agents.filter((agent) => agent.mode === 'primary'), [agents]);
|
||||||
const { isMobile, inputBarOffset, isKeyboardOpen, setTimelineDialogOpen, cornerRadius, persistChatDraft, isExpandedInput, setExpandedInput } = useUIStore();
|
const { isMobile, inputBarOffset, isKeyboardOpen, setTimelineDialogOpen, cornerRadius, persistChatDraft, inputSpellcheckEnabled, isExpandedInput, setExpandedInput } = useUIStore();
|
||||||
const { working } = useAssistantStatus();
|
const { working } = useAssistantStatus();
|
||||||
const { currentTheme } = useThemeSystem();
|
const { currentTheme } = useThemeSystem();
|
||||||
const chatSearchDirectory = useChatSearchDirectory();
|
const chatSearchDirectory = useChatSearchDirectory();
|
||||||
@@ -2682,7 +2682,7 @@ export const ChatInput: React.FC<ChatInputProps> = ({ onOpenSettings, scrollToBo
|
|||||||
disabled={!currentSessionId && !newSessionDraftOpen}
|
disabled={!currentSessionId && !newSessionDraftOpen}
|
||||||
autoCorrect={isMobile ? "on" : "off"}
|
autoCorrect={isMobile ? "on" : "off"}
|
||||||
autoCapitalize={isMobile ? "sentences" : "off"}
|
autoCapitalize={isMobile ? "sentences" : "off"}
|
||||||
spellCheck={isMobile}
|
spellCheck={isMobile || inputSpellcheckEnabled}
|
||||||
fillContainer={isDesktopExpanded}
|
fillContainer={isDesktopExpanded}
|
||||||
outerClassName={cn('focus-within:ring-0', isDesktopExpanded && 'flex-1 min-h-0')}
|
outerClassName={cn('focus-within:ring-0', isDesktopExpanded && 'flex-1 min-h-0')}
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -117,7 +117,7 @@ const VisualSectionContent: React.FC = () => {
|
|||||||
|
|
||||||
// Chat section: Default Tool Output, User message rendering, Diff layout, Mobile status bar, Show reasoning traces, Justification activity, Activity header timestamps, Queue mode, Persist draft
|
// Chat section: Default Tool Output, User message rendering, Diff layout, Mobile status bar, Show reasoning traces, Justification activity, Activity header timestamps, Queue mode, Persist draft
|
||||||
const ChatSectionContent: React.FC = () => {
|
const ChatSectionContent: React.FC = () => {
|
||||||
return <OpenChamberVisualSettings visibleSettings={['toolOutput', 'mermaidRendering', 'userMessageRendering', 'stickyUserHeader', 'diffLayout', 'mobileStatusBar', 'dotfiles', 'reasoning', 'textJustificationActivity', 'activityHeaderTimestamps', 'queueMode', 'persistDraft']} />;
|
return <OpenChamberVisualSettings visibleSettings={['toolOutput', 'mermaidRendering', 'userMessageRendering', 'stickyUserHeader', 'diffLayout', 'mobileStatusBar', 'dotfiles', 'reasoning', 'textJustificationActivity', 'activityHeaderTimestamps', 'queueMode', 'persistDraft', 'inputSpellcheck']} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sessions section: Default model & agent, Session retention, Memory limits
|
// Sessions section: Default model & agent, Session retention, Memory limits
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ const normalizeUserMessageRenderingMode = (mode: unknown): 'markdown' | 'plain'
|
|||||||
return mode === 'markdown' ? 'markdown' : 'plain';
|
return mode === 'markdown' ? 'markdown' : 'plain';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type VisibleSetting = 'theme' | 'pwaInstallName' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'cornerRadius' | 'inputBarOffset' | 'navRail' | 'toolOutput' | 'mermaidRendering' | 'userMessageRendering' | 'stickyUserHeader' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'reasoning' | 'queueMode' | 'textJustificationActivity' | 'activityHeaderTimestamps' | 'terminalQuickKeys' | 'persistDraft';
|
export type VisibleSetting = 'theme' | 'pwaInstallName' | 'fontSize' | 'terminalFontSize' | 'spacing' | 'cornerRadius' | 'inputBarOffset' | 'navRail' | 'toolOutput' | 'mermaidRendering' | 'userMessageRendering' | 'stickyUserHeader' | 'diffLayout' | 'mobileStatusBar' | 'dotfiles' | 'reasoning' | 'queueMode' | 'textJustificationActivity' | 'activityHeaderTimestamps' | 'terminalQuickKeys' | 'persistDraft' | 'inputSpellcheck';
|
||||||
|
|
||||||
interface OpenChamberVisualSettingsProps {
|
interface OpenChamberVisualSettingsProps {
|
||||||
/** Which settings to show. If undefined, shows all. */
|
/** Which settings to show. If undefined, shows all. */
|
||||||
@@ -169,6 +169,8 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
const setQueueMode = useMessageQueueStore(state => state.setQueueMode);
|
const setQueueMode = useMessageQueueStore(state => state.setQueueMode);
|
||||||
const persistChatDraft = useUIStore(state => state.persistChatDraft);
|
const persistChatDraft = useUIStore(state => state.persistChatDraft);
|
||||||
const setPersistChatDraft = useUIStore(state => state.setPersistChatDraft);
|
const setPersistChatDraft = useUIStore(state => state.setPersistChatDraft);
|
||||||
|
const inputSpellcheckEnabled = useUIStore(state => state.inputSpellcheckEnabled);
|
||||||
|
const setInputSpellcheckEnabled = useUIStore(state => state.setInputSpellcheckEnabled);
|
||||||
const isNavRailExpanded = useUIStore(state => state.isNavRailExpanded);
|
const isNavRailExpanded = useUIStore(state => state.isNavRailExpanded);
|
||||||
const setNavRailExpanded = useUIStore(state => state.setNavRailExpanded);
|
const setNavRailExpanded = useUIStore(state => state.setNavRailExpanded);
|
||||||
const showMobileSessionStatusBar = useUIStore(state => state.showMobileSessionStatusBar);
|
const showMobileSessionStatusBar = useUIStore(state => state.showMobileSessionStatusBar);
|
||||||
@@ -196,6 +198,11 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
void updateDesktopSettings({ stickyUserHeader: enabled });
|
void updateDesktopSettings({ stickyUserHeader: enabled });
|
||||||
}, [setStickyUserHeader]);
|
}, [setStickyUserHeader]);
|
||||||
|
|
||||||
|
const handleInputSpellcheckChange = React.useCallback((enabled: boolean) => {
|
||||||
|
setInputSpellcheckEnabled(enabled);
|
||||||
|
void updateDesktopSettings({ inputSpellcheckEnabled: enabled });
|
||||||
|
}, [setInputSpellcheckEnabled]);
|
||||||
|
|
||||||
const lightThemes = React.useMemo(
|
const lightThemes = React.useMemo(
|
||||||
() => availableThemes
|
() => availableThemes
|
||||||
.filter((theme) => theme.metadata.variant === 'light')
|
.filter((theme) => theme.metadata.variant === 'light')
|
||||||
@@ -245,7 +252,8 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
|| shouldShow('queueMode')
|
|| shouldShow('queueMode')
|
||||||
|| shouldShow('textJustificationActivity')
|
|| shouldShow('textJustificationActivity')
|
||||||
|| shouldShow('activityHeaderTimestamps')
|
|| shouldShow('activityHeaderTimestamps')
|
||||||
|| shouldShow('persistDraft');
|
|| shouldShow('persistDraft')
|
||||||
|
|| (!isMobile && shouldShow('inputSpellcheck'));
|
||||||
const selectedToolExpansionOption = TOOL_EXPANSION_OPTIONS.find((option) => option.value === toolCallExpansion);
|
const selectedToolExpansionOption = TOOL_EXPANSION_OPTIONS.find((option) => option.value === toolCallExpansion);
|
||||||
|
|
||||||
const showPwaInstallNameSetting = shouldShow('pwaInstallName') && isWebRuntime() && browserTab;
|
const showPwaInstallNameSetting = shouldShow('pwaInstallName') && isWebRuntime() && browserTab;
|
||||||
@@ -887,7 +895,7 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(shouldShow('stickyUserHeader') || (shouldShow('mobileStatusBar') && isMobile) || shouldShow('dotfiles') || shouldShow('queueMode') || shouldShow('persistDraft') || shouldShow('reasoning') || shouldShow('textJustificationActivity')) && (
|
{(shouldShow('stickyUserHeader') || (shouldShow('mobileStatusBar') && isMobile) || shouldShow('dotfiles') || shouldShow('queueMode') || shouldShow('persistDraft') || (!isMobile && shouldShow('inputSpellcheck')) || shouldShow('reasoning') || shouldShow('textJustificationActivity')) && (
|
||||||
<section className="p-2 space-y-0.5">
|
<section className="p-2 space-y-0.5">
|
||||||
{shouldShow('stickyUserHeader') && (
|
{shouldShow('stickyUserHeader') && (
|
||||||
<div
|
<div
|
||||||
@@ -1014,6 +1022,29 @@ export const OpenChamberVisualSettings: React.FC<OpenChamberVisualSettingsProps>
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!isMobile && shouldShow('inputSpellcheck') && (
|
||||||
|
<div
|
||||||
|
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-pressed={inputSpellcheckEnabled}
|
||||||
|
onClick={() => handleInputSpellcheckChange(!inputSpellcheckEnabled)}
|
||||||
|
onKeyDown={(event) => {
|
||||||
|
if (event.key === ' ' || event.key === 'Enter') {
|
||||||
|
event.preventDefault();
|
||||||
|
handleInputSpellcheckChange(!inputSpellcheckEnabled);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
checked={inputSpellcheckEnabled}
|
||||||
|
onChange={handleInputSpellcheckChange}
|
||||||
|
ariaLabel="Enable spellcheck in text inputs"
|
||||||
|
/>
|
||||||
|
<span className="typography-ui-label text-foreground">Enable Spellcheck in Text Inputs</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{shouldShow('reasoning') && (
|
{shouldShow('reasoning') && (
|
||||||
<div
|
<div
|
||||||
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
className="group flex cursor-pointer items-center gap-2 py-1.5"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useUIStore } from '@/stores/useUIStore';
|
||||||
|
|
||||||
interface CommitInputProps {
|
interface CommitInputProps {
|
||||||
value: string;
|
value: string;
|
||||||
@@ -8,6 +9,7 @@ interface CommitInputProps {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
hasTouchInput?: boolean;
|
hasTouchInput?: boolean;
|
||||||
|
isMobile?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIN_HEIGHT = 38; // Single line height
|
const MIN_HEIGHT = 38; // Single line height
|
||||||
@@ -19,8 +21,10 @@ export const CommitInput: React.FC<CommitInputProps> = ({
|
|||||||
placeholder = 'Commit message',
|
placeholder = 'Commit message',
|
||||||
disabled = false,
|
disabled = false,
|
||||||
hasTouchInput = false,
|
hasTouchInput = false,
|
||||||
|
isMobile = false,
|
||||||
}) => {
|
}) => {
|
||||||
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
|
const textareaRef = React.useRef<HTMLTextAreaElement>(null);
|
||||||
|
const inputSpellcheckEnabled = useUIStore((state) => state.inputSpellcheckEnabled);
|
||||||
|
|
||||||
// Auto-resize based on content (layout phase to avoid mount flicker)
|
// Auto-resize based on content (layout phase to avoid mount flicker)
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
@@ -45,7 +49,7 @@ export const CommitInput: React.FC<CommitInputProps> = ({
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
autoCorrect={hasTouchInput ? 'on' : 'off'}
|
autoCorrect={hasTouchInput ? 'on' : 'off'}
|
||||||
autoCapitalize={hasTouchInput ? 'sentences' : 'off'}
|
autoCapitalize={hasTouchInput ? 'sentences' : 'off'}
|
||||||
spellCheck={hasTouchInput ? true : false}
|
spellCheck={isMobile || inputSpellcheckEnabled}
|
||||||
scrollbarClassName="hidden"
|
scrollbarClassName="hidden"
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-lg bg-transparent resize-none overflow-y-hidden',
|
'rounded-lg bg-transparent resize-none overflow-y-hidden',
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ export const CommitSection: React.FC<CommitSectionProps> = ({
|
|||||||
placeholder="Commit message"
|
placeholder="Commit message"
|
||||||
disabled={commitAction !== null}
|
disabled={commitAction !== null}
|
||||||
hasTouchInput={hasTouchInput}
|
hasTouchInput={hasTouchInput}
|
||||||
|
isMobile={isMobile}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{gitmojiEnabled && (
|
{gitmojiEnabled && (
|
||||||
|
|||||||
@@ -525,6 +525,7 @@ export interface SettingsPayload {
|
|||||||
queueModeEnabled?: boolean;
|
queueModeEnabled?: boolean;
|
||||||
gitmojiEnabled?: boolean;
|
gitmojiEnabled?: boolean;
|
||||||
toolCallExpansion?: 'collapsed' | 'activity' | 'detailed' | 'changes';
|
toolCallExpansion?: 'collapsed' | 'activity' | 'detailed' | 'changes';
|
||||||
|
inputSpellcheckEnabled?: boolean;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
terminalFontSize?: number;
|
terminalFontSize?: number;
|
||||||
padding?: number;
|
padding?: number;
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export type DesktopSettings = {
|
|||||||
gitModelId?: string;
|
gitModelId?: string;
|
||||||
pwaAppName?: string;
|
pwaAppName?: string;
|
||||||
toolCallExpansion?: 'collapsed' | 'activity' | 'detailed' | 'changes';
|
toolCallExpansion?: 'collapsed' | 'activity' | 'detailed' | 'changes';
|
||||||
|
inputSpellcheckEnabled?: boolean;
|
||||||
userMessageRenderingMode?: 'markdown' | 'plain';
|
userMessageRenderingMode?: 'markdown' | 'plain';
|
||||||
stickyUserHeader?: boolean;
|
stickyUserHeader?: boolean;
|
||||||
fontSize?: number;
|
fontSize?: number;
|
||||||
|
|||||||
@@ -374,6 +374,9 @@ const applyDesktopUiPreferences = (settings: DesktopSettings) => {
|
|||||||
store.setToolCallExpansion(settings.toolCallExpansion);
|
store.setToolCallExpansion(settings.toolCallExpansion);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (typeof settings.inputSpellcheckEnabled === 'boolean' && settings.inputSpellcheckEnabled !== store.inputSpellcheckEnabled) {
|
||||||
|
store.setInputSpellcheckEnabled(settings.inputSpellcheckEnabled);
|
||||||
|
}
|
||||||
if (typeof settings.userMessageRenderingMode === 'string'
|
if (typeof settings.userMessageRenderingMode === 'string'
|
||||||
&& (settings.userMessageRenderingMode === 'markdown' || settings.userMessageRenderingMode === 'plain')) {
|
&& (settings.userMessageRenderingMode === 'markdown' || settings.userMessageRenderingMode === 'plain')) {
|
||||||
if (settings.userMessageRenderingMode !== store.userMessageRenderingMode) {
|
if (settings.userMessageRenderingMode !== store.userMessageRenderingMode) {
|
||||||
@@ -741,6 +744,9 @@ const sanitizeWebSettings = (payload: unknown): DesktopSettings | null => {
|
|||||||
) {
|
) {
|
||||||
result.toolCallExpansion = candidate.toolCallExpansion;
|
result.toolCallExpansion = candidate.toolCallExpansion;
|
||||||
}
|
}
|
||||||
|
if (typeof candidate.inputSpellcheckEnabled === 'boolean') {
|
||||||
|
result.inputSpellcheckEnabled = candidate.inputSpellcheckEnabled;
|
||||||
|
}
|
||||||
if (typeof candidate.userMessageRenderingMode === 'string'
|
if (typeof candidate.userMessageRenderingMode === 'string'
|
||||||
&& (candidate.userMessageRenderingMode === 'markdown' || candidate.userMessageRenderingMode === 'plain')) {
|
&& (candidate.userMessageRenderingMode === 'markdown' || candidate.userMessageRenderingMode === 'plain')) {
|
||||||
result.userMessageRenderingMode = candidate.userMessageRenderingMode;
|
result.userMessageRenderingMode = candidate.userMessageRenderingMode;
|
||||||
|
|||||||
@@ -550,6 +550,7 @@ interface UIStore {
|
|||||||
|
|
||||||
showTerminalQuickKeysOnDesktop: boolean;
|
showTerminalQuickKeysOnDesktop: boolean;
|
||||||
persistChatDraft: boolean;
|
persistChatDraft: boolean;
|
||||||
|
inputSpellcheckEnabled: boolean;
|
||||||
mermaidRenderingMode: MermaidRenderingMode;
|
mermaidRenderingMode: MermaidRenderingMode;
|
||||||
userMessageRenderingMode: UserMessageRenderingMode;
|
userMessageRenderingMode: UserMessageRenderingMode;
|
||||||
stickyUserHeader: boolean;
|
stickyUserHeader: boolean;
|
||||||
@@ -660,6 +661,7 @@ interface UIStore {
|
|||||||
setSummaryLength: (value: number) => void;
|
setSummaryLength: (value: number) => void;
|
||||||
setMaxLastMessageLength: (value: number) => void;
|
setMaxLastMessageLength: (value: number) => void;
|
||||||
setPersistChatDraft: (value: boolean) => void;
|
setPersistChatDraft: (value: boolean) => void;
|
||||||
|
setInputSpellcheckEnabled: (value: boolean) => void;
|
||||||
setMermaidRenderingMode: (value: MermaidRenderingMode) => void;
|
setMermaidRenderingMode: (value: MermaidRenderingMode) => void;
|
||||||
setUserMessageRenderingMode: (value: UserMessageRenderingMode) => void;
|
setUserMessageRenderingMode: (value: UserMessageRenderingMode) => void;
|
||||||
setStickyUserHeader: (value: boolean) => void;
|
setStickyUserHeader: (value: boolean) => void;
|
||||||
@@ -770,6 +772,7 @@ export const useUIStore = create<UIStore>()(
|
|||||||
|
|
||||||
showTerminalQuickKeysOnDesktop: false,
|
showTerminalQuickKeysOnDesktop: false,
|
||||||
persistChatDraft: true,
|
persistChatDraft: true,
|
||||||
|
inputSpellcheckEnabled: false,
|
||||||
mermaidRenderingMode: 'svg',
|
mermaidRenderingMode: 'svg',
|
||||||
userMessageRenderingMode: 'markdown',
|
userMessageRenderingMode: 'markdown',
|
||||||
stickyUserHeader: true,
|
stickyUserHeader: true,
|
||||||
@@ -1668,6 +1671,9 @@ export const useUIStore = create<UIStore>()(
|
|||||||
setPersistChatDraft: (value) => {
|
setPersistChatDraft: (value) => {
|
||||||
set({ persistChatDraft: value });
|
set({ persistChatDraft: value });
|
||||||
},
|
},
|
||||||
|
setInputSpellcheckEnabled: (value) => {
|
||||||
|
set({ inputSpellcheckEnabled: value });
|
||||||
|
},
|
||||||
setMermaidRenderingMode: (value) => {
|
setMermaidRenderingMode: (value) => {
|
||||||
set({ mermaidRenderingMode: value });
|
set({ mermaidRenderingMode: value });
|
||||||
},
|
},
|
||||||
@@ -1854,6 +1860,7 @@ export const useUIStore = create<UIStore>()(
|
|||||||
summaryLength: state.summaryLength,
|
summaryLength: state.summaryLength,
|
||||||
maxLastMessageLength: state.maxLastMessageLength,
|
maxLastMessageLength: state.maxLastMessageLength,
|
||||||
persistChatDraft: state.persistChatDraft,
|
persistChatDraft: state.persistChatDraft,
|
||||||
|
inputSpellcheckEnabled: state.inputSpellcheckEnabled,
|
||||||
mermaidRenderingMode: state.mermaidRenderingMode,
|
mermaidRenderingMode: state.mermaidRenderingMode,
|
||||||
userMessageRenderingMode: state.userMessageRenderingMode,
|
userMessageRenderingMode: state.userMessageRenderingMode,
|
||||||
stickyUserHeader: state.stickyUserHeader,
|
stickyUserHeader: state.stickyUserHeader,
|
||||||
|
|||||||
@@ -2094,6 +2094,9 @@ const sanitizeSettingsUpdate = (payload) => {
|
|||||||
result.toolCallExpansion = mode;
|
result.toolCallExpansion = mode;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (typeof candidate.inputSpellcheckEnabled === 'boolean') {
|
||||||
|
result.inputSpellcheckEnabled = candidate.inputSpellcheckEnabled;
|
||||||
|
}
|
||||||
if (typeof candidate.userMessageRenderingMode === 'string') {
|
if (typeof candidate.userMessageRenderingMode === 'string') {
|
||||||
const mode = candidate.userMessageRenderingMode.trim();
|
const mode = candidate.userMessageRenderingMode.trim();
|
||||||
if (mode === 'markdown' || mode === 'plain') {
|
if (mode === 'markdown' || mode === 'plain') {
|
||||||
|
|||||||
Reference in New Issue
Block a user