[eric] settings: Appearance keeps only theme/text/accent; Dictation, Canvas, and Agents get their own rail pages

This commit is contained in:
ciregenz
2026-08-05 23:50:56 -07:00
parent 889b8d6738
commit 69805dfbe7
6 changed files with 343 additions and 248 deletions
@@ -14,6 +14,9 @@ import { CommandsContent } from '@/app/pages/Commands/Commands';
import AccountCard from './sections/subscription/AccountCard';
import GeneralAgentDefaults from './sections/general/GeneralAgentDefaults';
import GeneralInterface from './sections/general/GeneralInterface';
import DictationSettings from './sections/general/DictationSettings';
import CanvasSettings from './sections/general/CanvasSettings';
import AgentBehaviorSettings from './sections/general/AgentBehaviorSettings';
import GeneralAdvanced from './sections/general/GeneralAdvanced';
import DataPrivacySection from './sections/general/DataPrivacySection';
import ModelsTab from './sections/models/ModelsTab';
@@ -29,7 +32,7 @@ import { PROVIDER_COLORS, OPENSWARM_GRADIENT, useModelOptions } from './settings
// Module-scope: remember the last open tab across closes (System Settings style).
let lastOpenTab: string | null = null;
const TAB_VALUES = ['account', 'general', 'appearance', 'notifications', 'privacy', 'advanced', 'models', 'commands', 'usage'] as const;
const TAB_VALUES = ['account', 'general', 'appearance', 'dictation', 'canvas', 'agents', 'notifications', 'privacy', 'advanced', 'models', 'commands', 'usage'] as const;
type SettingsTab = typeof TAB_VALUES[number];
const isValidTab = (t: string | null | undefined): t is SettingsTab =>
!!t && (TAB_VALUES as readonly string[]).includes(t);
@@ -132,6 +135,18 @@ const SettingsBody: React.FC<SettingsBodyProps> = ({ active, onRequestClose }) =
<Box sx={{ pt: 0.5, pb: 2, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
<GeneralInterface form={form} setForm={setForm} styles={styles} />
</Box>
) : activeTab === 'dictation' ? (
<Box sx={{ pt: 0.5, pb: 2, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
<DictationSettings form={form} setForm={setForm} styles={styles} />
</Box>
) : activeTab === 'canvas' ? (
<Box sx={{ pt: 0.5, pb: 2, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
<CanvasSettings form={form} setForm={setForm} styles={styles} />
</Box>
) : activeTab === 'agents' ? (
<Box sx={{ pt: 0.5, pb: 2, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
<AgentBehaviorSettings form={form} setForm={setForm} styles={styles} />
</Box>
) : activeTab === 'privacy' ? (
<Box sx={{ pt: 0.5, pb: 2, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
<DataPrivacySection form={form} setForm={setForm} styles={styles} />
@@ -1,7 +1,7 @@
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { User, Settings2, Palette, ShieldCheck, Wrench, Boxes, SquareSlash, BarChart3, Bell } from 'lucide-react';
import { User, Settings2, Palette, ShieldCheck, Wrench, Boxes, SquareSlash, BarChart3, Bell, Mic, LayoutGrid, Bot } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
@@ -25,6 +25,9 @@ export const RAIL_GROUPS: RailGroup[] = [
{ header: 'App', sections: [
{ value: 'general', label: 'General', Icon: Settings2 },
{ value: 'appearance', label: 'Appearance', Icon: Palette },
{ value: 'dictation', label: 'Dictation', Icon: Mic },
{ value: 'canvas', label: 'Canvas', Icon: LayoutGrid },
{ value: 'agents', label: 'Agents', Icon: Bot },
{ value: 'notifications', label: 'Notifications', Icon: Bell },
{ value: 'privacy', label: 'Privacy', Icon: ShieldCheck },
{ value: 'advanced', label: 'Advanced', Icon: Wrench },
@@ -0,0 +1,88 @@
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import { AppSettings } from '@/shared/state/settingsSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import type { SettingsStyles } from '../settingsStyles';
import { settingSelectAttrs } from '../settingSelect';
import ShortcutRecorderChip from './parts/ShortcutRecorderChip';
const AgentBehaviorSettings: React.FC<{
form: AppSettings;
setForm: React.Dispatch<React.SetStateAction<AppSettings>>;
styles: SettingsStyles;
}> = ({ form, setForm, styles }) => {
const c = useClaudeTokens();
const { inlineRowSx, inlineRowLastSx, labelSx, descSx, toggleGroupSx } = styles;
return (
<>
<Box sx={inlineRowSx} {...settingSelectAttrs('new_agent_shortcut', 'New agent shortcut', 'Interface', 'Keyboard shortcut to create an agent.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>New agent shortcut</Typography>
<Typography sx={descSx}>Keyboard shortcut to create an agent.</Typography>
</Box>
<ShortcutRecorderChip
value={form.new_agent_shortcut}
onChange={(combo) => setForm({ ...form, new_agent_shortcut: combo })}
/>
</Box>
<Box sx={inlineRowSx} {...settingSelectAttrs('auto_select_mode_on_new_agent', 'Auto-enable element selection', 'Interface', 'Enter element selection mode when creating a new agent.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Auto-enable element selection</Typography>
<Typography sx={descSx}>Automatically enter element selection mode when creating a new agent.</Typography>
</Box>
<ToggleButtonGroup
value={form.auto_select_mode_on_new_agent ?? false}
exclusive
onChange={(_, v) => { if (v !== null) setForm({ ...form, auto_select_mode_on_new_agent: v }); }}
size="small"
sx={toggleGroupSx}
>
<ToggleButton value={true}>On</ToggleButton>
<ToggleButton value={false}>Off</ToggleButton>
</ToggleButtonGroup>
</Box>
<Box sx={inlineRowSx} {...settingSelectAttrs('expand_new_chats_in_dashboard', 'Default agent spawn state in dashboard', 'Interface', 'New agents spawn expanded instead of collapsed.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Default agent spawn state in dashboard</Typography>
<Typography sx={descSx}>When enabled, new agents spawn expanded instead of collapsed.</Typography>
</Box>
{/* Named for the state you get, not on/off: "spawn state" has no obvious on. */}
<ToggleButtonGroup
value={form.expand_new_chats_in_dashboard ?? false}
exclusive
onChange={(_, v) => { if (v !== null) setForm({ ...form, expand_new_chats_in_dashboard: v }); }}
size="small"
sx={toggleGroupSx}
>
<ToggleButton value={true}>Expanded</ToggleButton>
<ToggleButton value={false}>Collapsed</ToggleButton>
</ToggleButtonGroup>
</Box>
<Box sx={inlineRowLastSx} {...settingSelectAttrs('auto_reveal_sub_agents', 'Auto-reveal sub-agents on dashboard', 'Interface', 'Show sub-agent cards tethered to their parent on the dashboard.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Auto-reveal sub-agents on dashboard</Typography>
<Typography sx={descSx}>Automatically show sub-agent cards (from CreateAgent / InvokeAgent) tethered to their parent on the dashboard.</Typography>
</Box>
<ToggleButtonGroup
value={form.auto_reveal_sub_agents ?? false}
exclusive
onChange={(_, v) => { if (v !== null) setForm({ ...form, auto_reveal_sub_agents: v }); }}
size="small"
sx={toggleGroupSx}
>
<ToggleButton value={true}>Show</ToggleButton>
<ToggleButton value={false}>Hide</ToggleButton>
</ToggleButtonGroup>
</Box>
</>
);
};
export default AgentBehaviorSettings;
@@ -0,0 +1,99 @@
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import TextField from '@mui/material/TextField';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import Slider from '@mui/material/Slider';
import LanguageIcon from '@mui/icons-material/Language';
import { AppSettings } from '@/shared/state/settingsSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import type { SettingsStyles } from '../settingsStyles';
import { settingSelectAttrs } from '../settingSelect';
const CanvasSettings: React.FC<{
form: AppSettings;
setForm: React.Dispatch<React.SetStateAction<AppSettings>>;
styles: SettingsStyles;
}> = ({ form, setForm, styles }) => {
const c = useClaudeTokens();
const { fieldSx, sectionSx, rowSx, rowLastSx, inlineRowSx, labelSx, descSx, toggleGroupSx } = styles;
return (
<>
<Box sx={inlineRowSx} {...settingSelectAttrs('mouse_wheel_action', 'Mouse wheel', 'Interface', 'What a mouse wheel does on the dashboard canvas.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Mouse wheel</Typography>
<Typography sx={descSx}>
What a plain mouse wheel does on the canvas. The other one moves to cmd/ctrl + wheel.
A trackpad two-finger scroll always pans, and pinch always zooms.
</Typography>
</Box>
<ToggleButtonGroup
value={form.mouse_wheel_action ?? 'zoom'}
exclusive
onChange={(_, v) => { if (v !== null) setForm({ ...form, mouse_wheel_action: v }); }}
size="small"
sx={toggleGroupSx}
>
<ToggleButton value="zoom">Zoom</ToggleButton>
<ToggleButton value="scroll">Scroll</ToggleButton>
</ToggleButtonGroup>
</Box>
<Box sx={rowSx} {...settingSelectAttrs('zoom_sensitivity', 'Zoom sensitivity', 'Interface', 'Scroll-to-zoom responsiveness.')}>
<Typography sx={labelSx}>Zoom sensitivity</Typography>
<Typography sx={{ ...descSx, mb: 1 }}>
Scroll-to-zoom responsiveness. Lower for trackpads, higher for mouse wheels.
</Typography>
<Box sx={{ px: 1 }}>
<Slider
value={form.zoom_sensitivity}
onChange={(_, v) => setForm({ ...form, zoom_sensitivity: v as number })}
min={1}
max={100}
step={1}
valueLabelDisplay="auto"
marks={[
{ value: 1, label: 'Low' },
{ value: 50, label: 'Default' },
{ value: 100, label: 'High' },
]}
sx={{
color: c.accent.primary,
'& .MuiSlider-markLabel': { color: c.text.tertiary, fontSize: '0.6875rem' },
'& .MuiSlider-valueLabel': { bgcolor: c.accent.primary },
}}
/>
</Box>
</Box>
<Typography sx={{ ...sectionSx, mt: 3 }}>Browser</Typography>
<Box sx={rowLastSx} {...settingSelectAttrs('browser_homepage', 'Default homepage', 'Browser', 'URL loaded when opening a new browser card.')}>
<Typography sx={labelSx}>Default homepage</Typography>
<Typography sx={{ ...descSx, mb: 1.5 }}>
URL loaded when opening a new browser card on the dashboard.
</Typography>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<LanguageIcon sx={{ fontSize: 18, color: c.text.tertiary, flexShrink: 0 }} />
<TextField
value={form.browser_homepage}
onChange={(e) => setForm({ ...form, browser_homepage: e.target.value })}
size="small"
fullWidth
placeholder="https://www.google.com"
sx={{
...fieldSx,
'& .MuiOutlinedInput-root': {
...fieldSx['& .MuiOutlinedInput-root'],
fontFamily: c.font.mono,
},
}}
/>
</Box>
</Box>
</>
);
};
export default CanvasSettings;
@@ -0,0 +1,136 @@
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import TextField from '@mui/material/TextField';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import Slider from '@mui/material/Slider';
import Switch from '@mui/material/Switch';
import { AppSettings } from '@/shared/state/settingsSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import type { SettingsStyles } from '../settingsStyles';
import { settingSelectAttrs } from '../settingSelect';
import ShortcutRecorderChip, { dictationDefaultCombo, comboDisplay } from './parts/ShortcutRecorderChip';
import DictationModelPicker from './parts/DictationModelPicker';
import DictationHistoryList from './parts/DictationHistoryList';
const DictationSettings: React.FC<{
form: AppSettings;
setForm: React.Dispatch<React.SetStateAction<AppSettings>>;
styles: SettingsStyles;
}> = ({ form, setForm, styles }) => {
const c = useClaudeTokens();
const { inlineRowSx, labelSx, descSx, toggleGroupSx } = styles;
return (
<>
<Box sx={inlineRowSx} {...settingSelectAttrs('voice_hold_to_talk', 'Dictation', 'Interface', 'Hold to talk, or tap to start and stop.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Dictation</Typography>
<Typography sx={descSx}>{`How the mic button and the dictation shortcut (${comboDisplay(form.dictation_shortcut || dictationDefaultCombo())}) work.`}</Typography>
</Box>
<ToggleButtonGroup
value={form.voice_hold_to_talk ?? true}
exclusive
onChange={(_, v) => {
if (v === null) return;
setForm({ ...form, voice_hold_to_talk: v });
// Keyboard hold needs the native key tap; picking Hold on a Mac without the Accessibility
// grant fires the system prompt so the choice can actually take effect after a relaunch.
if (v) void window.openswarm?.voiceRequestHoldPermission?.();
}}
size="small"
sx={toggleGroupSx}
>
<ToggleButton value={true}>Hold to talk</ToggleButton>
<ToggleButton value={false}>Tap to toggle</ToggleButton>
</ToggleButtonGroup>
</Box>
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_shortcut', 'Dictation shortcut', 'Interface', 'Global hotkey that starts dictation.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Dictation shortcut</Typography>
<Typography sx={descSx}>Works anywhere, even with the app in the background.</Typography>
</Box>
<ShortcutRecorderChip
value={form.dictation_shortcut || dictationDefaultCombo()}
onChange={(combo) => setForm({ ...form, dictation_shortcut: combo })}
/>
</Box>
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_model', 'Dictation model', 'Interface', 'Speech model used for dictation; bigger is more accurate but slower.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Dictation model</Typography>
<Typography sx={descSx}>Runs on this machine, nothing is uploaded. Bigger is more accurate but slower to transcribe.</Typography>
</Box>
<DictationModelPicker
value={form.dictation_model ?? null}
onChange={(id) => setForm({ ...form, dictation_model: id })}
/>
</Box>
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_dictionary', 'Dictation dictionary', 'Interface', 'Names and jargon dictation should always spell right.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Dictionary</Typography>
<Typography sx={descSx}>Comma-separated names and jargon (people, products, acronyms) that dictation should always spell right.</Typography>
</Box>
<TextField
size="small"
placeholder="Anthropic, Kubernetes, OpenSwarm"
value={form.dictation_dictionary ?? ''}
onChange={(e) => setForm({ ...form, dictation_dictionary: e.target.value })}
sx={{ width: 280 }}
/>
</Box>
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_sounds', 'Dictation sounds', 'Interface', 'The start, stop, and text-landed cues.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Sounds</Typography>
<Typography sx={descSx}>The start, stop, and text-landed cues, and how loud they play.</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Slider
size="small"
min={0}
max={100}
disabled={!(form.dictation_sounds ?? true)}
value={Math.round((form.dictation_sound_volume ?? 0.35) * 100)}
onChange={(_, v) => setForm({ ...form, dictation_sound_volume: (v as number) / 100 })}
sx={{ width: 110 }}
/>
<Switch
size="small"
checked={form.dictation_sounds ?? true}
onChange={(e) => setForm({ ...form, dictation_sounds: e.target.checked })}
/>
</Box>
</Box>
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_disabled_surfaces', 'Dictation off for sites', 'Interface', 'Sites where the dictation key does nothing.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Off for sites</Typography>
<Typography sx={descSx}>Comma-separated hostnames where the dictation key refuses to record (it tells you instead of failing silently).</Typography>
</Box>
<TextField
size="small"
placeholder="docs.google.com, slack.com"
value={form.dictation_disabled_surfaces ?? ''}
onChange={(e) => setForm({ ...form, dictation_disabled_surfaces: e.target.value })}
sx={{ width: 280 }}
/>
</Box>
<Box sx={{ ...inlineRowSx, alignItems: 'flex-start' }} {...settingSelectAttrs('dictation_history', 'Dictation history', 'Interface', 'Your recent dictations, copyable.')}>
<Box sx={{ mr: 3, flexShrink: 0, width: 220 }}>
<Typography sx={labelSx}>History</Typography>
<Typography sx={descSx}>Recent dictations, stored only on this machine. Copy one back if it landed in the wrong place.</Typography>
</Box>
<DictationHistoryList />
</Box>
</>
);
};
export default DictationSettings;
@@ -95,252 +95,6 @@ const GeneralInterface: React.FC<{
/>
</Box>
<Typography sx={sectionSx}>Dictation</Typography>
<Box sx={inlineRowSx} {...settingSelectAttrs('voice_hold_to_talk', 'Dictation', 'Interface', 'Hold to talk, or tap to start and stop.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Dictation</Typography>
<Typography sx={descSx}>{`How the mic button and the dictation shortcut (${comboDisplay(form.dictation_shortcut || dictationDefaultCombo())}) work.`}</Typography>
</Box>
<ToggleButtonGroup
value={form.voice_hold_to_talk ?? true}
exclusive
onChange={(_, v) => {
if (v === null) return;
setForm({ ...form, voice_hold_to_talk: v });
// Keyboard hold needs the native key tap; picking Hold on a Mac without the Accessibility
// grant fires the system prompt so the choice can actually take effect after a relaunch.
if (v) void window.openswarm?.voiceRequestHoldPermission?.();
}}
size="small"
sx={toggleGroupSx}
>
<ToggleButton value={true}>Hold to talk</ToggleButton>
<ToggleButton value={false}>Tap to toggle</ToggleButton>
</ToggleButtonGroup>
</Box>
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_shortcut', 'Dictation shortcut', 'Interface', 'Global hotkey that starts dictation.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Dictation shortcut</Typography>
<Typography sx={descSx}>Works anywhere, even with the app in the background.</Typography>
</Box>
<ShortcutRecorderChip
value={form.dictation_shortcut || dictationDefaultCombo()}
onChange={(combo) => setForm({ ...form, dictation_shortcut: combo })}
/>
</Box>
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_model', 'Dictation model', 'Interface', 'Speech model used for dictation; bigger is more accurate but slower.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Dictation model</Typography>
<Typography sx={descSx}>Runs on this machine, nothing is uploaded. Bigger is more accurate but slower to transcribe.</Typography>
</Box>
<DictationModelPicker
value={form.dictation_model ?? null}
onChange={(id) => setForm({ ...form, dictation_model: id })}
/>
</Box>
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_dictionary', 'Dictation dictionary', 'Interface', 'Names and jargon dictation should always spell right.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Dictionary</Typography>
<Typography sx={descSx}>Comma-separated names and jargon (people, products, acronyms) that dictation should always spell right.</Typography>
</Box>
<TextField
size="small"
placeholder="Anthropic, Kubernetes, OpenSwarm"
value={form.dictation_dictionary ?? ''}
onChange={(e) => setForm({ ...form, dictation_dictionary: e.target.value })}
sx={{ width: 280 }}
/>
</Box>
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_sounds', 'Dictation sounds', 'Interface', 'The start, stop, and text-landed cues.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Sounds</Typography>
<Typography sx={descSx}>The start, stop, and text-landed cues, and how loud they play.</Typography>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1.5 }}>
<Slider
size="small"
min={0}
max={100}
disabled={!(form.dictation_sounds ?? true)}
value={Math.round((form.dictation_sound_volume ?? 0.35) * 100)}
onChange={(_, v) => setForm({ ...form, dictation_sound_volume: (v as number) / 100 })}
sx={{ width: 110 }}
/>
<Switch
size="small"
checked={form.dictation_sounds ?? true}
onChange={(e) => setForm({ ...form, dictation_sounds: e.target.checked })}
/>
</Box>
</Box>
<Box sx={inlineRowSx} {...settingSelectAttrs('dictation_disabled_surfaces', 'Dictation off for sites', 'Interface', 'Sites where the dictation key does nothing.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Off for sites</Typography>
<Typography sx={descSx}>Comma-separated hostnames where the dictation key refuses to record (it tells you instead of failing silently).</Typography>
</Box>
<TextField
size="small"
placeholder="docs.google.com, slack.com"
value={form.dictation_disabled_surfaces ?? ''}
onChange={(e) => setForm({ ...form, dictation_disabled_surfaces: e.target.value })}
sx={{ width: 280 }}
/>
</Box>
<Box sx={{ ...inlineRowSx, alignItems: 'flex-start' }} {...settingSelectAttrs('dictation_history', 'Dictation history', 'Interface', 'Your recent dictations, copyable.')}>
<Box sx={{ mr: 3, flexShrink: 0, width: 220 }}>
<Typography sx={labelSx}>History</Typography>
<Typography sx={descSx}>Recent dictations, stored only on this machine. Copy one back if it landed in the wrong place.</Typography>
</Box>
<DictationHistoryList />
</Box>
<Typography sx={sectionSx}>Canvas</Typography>
<Box sx={inlineRowSx} {...settingSelectAttrs('mouse_wheel_action', 'Mouse wheel', 'Interface', 'What a mouse wheel does on the dashboard canvas.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Mouse wheel</Typography>
<Typography sx={descSx}>
What a plain mouse wheel does on the canvas. The other one moves to cmd/ctrl + wheel.
A trackpad two-finger scroll always pans, and pinch always zooms.
</Typography>
</Box>
<ToggleButtonGroup
value={form.mouse_wheel_action ?? 'zoom'}
exclusive
onChange={(_, v) => { if (v !== null) setForm({ ...form, mouse_wheel_action: v }); }}
size="small"
sx={toggleGroupSx}
>
<ToggleButton value="zoom">Zoom</ToggleButton>
<ToggleButton value="scroll">Scroll</ToggleButton>
</ToggleButtonGroup>
</Box>
<Box sx={rowSx} {...settingSelectAttrs('zoom_sensitivity', 'Zoom sensitivity', 'Interface', 'Scroll-to-zoom responsiveness.')}>
<Typography sx={labelSx}>Zoom sensitivity</Typography>
<Typography sx={{ ...descSx, mb: 1 }}>
Scroll-to-zoom responsiveness. Lower for trackpads, higher for mouse wheels.
</Typography>
<Box sx={{ px: 1 }}>
<Slider
value={form.zoom_sensitivity}
onChange={(_, v) => setForm({ ...form, zoom_sensitivity: v as number })}
min={1}
max={100}
step={1}
valueLabelDisplay="auto"
marks={[
{ value: 1, label: 'Low' },
{ value: 50, label: 'Default' },
{ value: 100, label: 'High' },
]}
sx={{
color: c.accent.primary,
'& .MuiSlider-markLabel': { color: c.text.tertiary, fontSize: '0.6875rem' },
'& .MuiSlider-valueLabel': { bgcolor: c.accent.primary },
}}
/>
</Box>
</Box>
<Typography sx={sectionSx}>Agents</Typography>
<Box sx={inlineRowSx} {...settingSelectAttrs('new_agent_shortcut', 'New agent shortcut', 'Interface', 'Keyboard shortcut to create an agent.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>New agent shortcut</Typography>
<Typography sx={descSx}>Keyboard shortcut to create an agent.</Typography>
</Box>
<ShortcutRecorderChip
value={form.new_agent_shortcut}
onChange={(combo) => setForm({ ...form, new_agent_shortcut: combo })}
/>
</Box>
<Box sx={inlineRowSx} {...settingSelectAttrs('auto_select_mode_on_new_agent', 'Auto-enable element selection', 'Interface', 'Enter element selection mode when creating a new agent.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Auto-enable element selection</Typography>
<Typography sx={descSx}>Automatically enter element selection mode when creating a new agent.</Typography>
</Box>
<ToggleButtonGroup
value={form.auto_select_mode_on_new_agent ?? false}
exclusive
onChange={(_, v) => { if (v !== null) setForm({ ...form, auto_select_mode_on_new_agent: v }); }}
size="small"
sx={toggleGroupSx}
>
<ToggleButton value={true}>On</ToggleButton>
<ToggleButton value={false}>Off</ToggleButton>
</ToggleButtonGroup>
</Box>
<Box sx={inlineRowSx} {...settingSelectAttrs('expand_new_chats_in_dashboard', 'Default agent spawn state in dashboard', 'Interface', 'New agents spawn expanded instead of collapsed.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Default agent spawn state in dashboard</Typography>
<Typography sx={descSx}>When enabled, new agents spawn expanded instead of collapsed.</Typography>
</Box>
{/* Named for the state you get, not on/off: "spawn state" has no obvious on. */}
<ToggleButtonGroup
value={form.expand_new_chats_in_dashboard ?? false}
exclusive
onChange={(_, v) => { if (v !== null) setForm({ ...form, expand_new_chats_in_dashboard: v }); }}
size="small"
sx={toggleGroupSx}
>
<ToggleButton value={true}>Expanded</ToggleButton>
<ToggleButton value={false}>Collapsed</ToggleButton>
</ToggleButtonGroup>
</Box>
<Box sx={inlineRowLastSx} {...settingSelectAttrs('auto_reveal_sub_agents', 'Auto-reveal sub-agents on dashboard', 'Interface', 'Show sub-agent cards tethered to their parent on the dashboard.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Auto-reveal sub-agents on dashboard</Typography>
<Typography sx={descSx}>Automatically show sub-agent cards (from CreateAgent / InvokeAgent) tethered to their parent on the dashboard.</Typography>
</Box>
<ToggleButtonGroup
value={form.auto_reveal_sub_agents ?? false}
exclusive
onChange={(_, v) => { if (v !== null) setForm({ ...form, auto_reveal_sub_agents: v }); }}
size="small"
sx={toggleGroupSx}
>
<ToggleButton value={true}>Show</ToggleButton>
<ToggleButton value={false}>Hide</ToggleButton>
</ToggleButtonGroup>
</Box>
<Typography sx={{ ...sectionSx, mt: 3 }}>Browser</Typography>
<Box sx={rowLastSx} {...settingSelectAttrs('browser_homepage', 'Default homepage', 'Browser', 'URL loaded when opening a new browser card.')}>
<Typography sx={labelSx}>Default homepage</Typography>
<Typography sx={{ ...descSx, mb: 1.5 }}>
URL loaded when opening a new browser card on the dashboard.
</Typography>
<Box sx={{ display: 'flex', gap: 1, alignItems: 'center' }}>
<LanguageIcon sx={{ fontSize: 18, color: c.text.tertiary, flexShrink: 0 }} />
<TextField
value={form.browser_homepage}
onChange={(e) => setForm({ ...form, browser_homepage: e.target.value })}
size="small"
fullWidth
placeholder="https://www.google.com"
sx={{
...fieldSx,
'& .MuiOutlinedInput-root': {
...fieldSx['& .MuiOutlinedInput-root'],
fontFamily: c.font.mono,
},
}}
/>
</Box>
</Box>
</>
);
};