feat(settings): add Display panel for output/visual settings

Split General settings panel by creating new Display tab containing:
- Max output lines per response
- Document graph settings (external links, max nodes)
- Context window warnings (thresholds)

Updated docs to reflect new panel location.
This commit is contained in:
Pedram Amini
2026-02-02 02:49:15 -06:00
parent dd340da30f
commit 6a913b9b04
3 changed files with 302 additions and 288 deletions

View File

@@ -12,7 +12,8 @@ Settings are organized into tabs:
| Tab | Contents |
|-----|----------|
| **General** | Font family and size, terminal width, log level and buffer, max output lines, shell configuration, input send behavior, default toggles (history, thinking), automatic tab naming, power management, updates, privacy, context warnings, usage stats, document graph, storage location |
| **General** | Font family and size, terminal width, log level and buffer, shell configuration, input send behavior, default toggles (history, thinking), automatic tab naming, power management, updates, privacy, usage stats, storage location |
| **Display** | Max output lines per response, document graph settings, context window warnings |
| **Shortcuts** | Customize keyboard shortcuts (see [Keyboard Shortcuts](./keyboard-shortcuts)) |
| **Themes** | Dark, light, and vibe mode themes, custom theme builder with import/export |
| **Notifications** | OS notifications, custom command notifications, toast notification duration |

View File

@@ -105,7 +105,7 @@ For best results, **compact your context before reaching 60-70% usage** — don'
### Configuring Warnings
Customize warning thresholds in **Settings** (`Cmd+,` / `Ctrl+,`) → **General****Context Window Warnings**:
Customize warning thresholds in **Settings** (`Cmd+,` / `Ctrl+,`) → **Display****Context Window Warnings**:
![Context Warning Configuration](./screenshots/context-warnings-config.png)

View File

@@ -272,7 +272,7 @@ interface SettingsModalProps {
setCrashReportingEnabled: (value: boolean) => void;
customAICommands: CustomAICommand[];
setCustomAICommands: (commands: CustomAICommand[]) => void;
initialTab?: 'general' | 'llm' | 'shortcuts' | 'theme' | 'notifications' | 'aicommands' | 'ssh';
initialTab?: 'general' | 'display' | 'llm' | 'shortcuts' | 'theme' | 'notifications' | 'aicommands' | 'ssh';
hasNoAgents?: boolean;
onThemeImportError?: (message: string) => void;
onThemeImportSuccess?: (message: string) => void;
@@ -314,7 +314,7 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro
} = useSettings();
const [activeTab, setActiveTab] = useState<
'general' | 'llm' | 'shortcuts' | 'theme' | 'notifications' | 'aicommands' | 'ssh'
'general' | 'display' | 'llm' | 'shortcuts' | 'theme' | 'notifications' | 'aicommands' | 'ssh'
>('general');
const [systemFonts, setSystemFonts] = useState<string[]>([]);
const [customFonts, setCustomFonts] = useState<string[]>([]);
@@ -468,10 +468,10 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro
const handleTabNavigation = (e: KeyboardEvent) => {
const tabs: Array<
'general' | 'llm' | 'shortcuts' | 'theme' | 'notifications' | 'aicommands' | 'ssh'
'general' | 'display' | 'llm' | 'shortcuts' | 'theme' | 'notifications' | 'aicommands' | 'ssh'
> = FEATURE_FLAGS.LLM_SETTINGS
? ['general', 'llm', 'shortcuts', 'theme', 'notifications', 'aicommands', 'ssh']
: ['general', 'shortcuts', 'theme', 'notifications', 'aicommands', 'ssh'];
? ['general', 'display', 'llm', 'shortcuts', 'theme', 'notifications', 'aicommands', 'ssh']
: ['general', 'display', 'shortcuts', 'theme', 'notifications', 'aicommands', 'ssh'];
const currentIndex = tabs.indexOf(activeTab);
if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === '[') {
@@ -877,6 +877,15 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro
<Settings className="w-4 h-4" />
{activeTab === 'general' && <span>General</span>}
</button>
<button
onClick={() => setActiveTab('display')}
className={`px-4 py-4 text-sm font-bold border-b-2 ${activeTab === 'display' ? 'border-indigo-500' : 'border-transparent'} flex items-center gap-2`}
tabIndex={-1}
title="Display"
>
<Monitor className="w-4 h-4" />
{activeTab === 'display' && <span>Display</span>}
</button>
{FEATURE_FLAGS.LLM_SETTINGS && (
<button
onClick={() => setActiveTab('llm')}
@@ -1025,29 +1034,6 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro
</p>
</div>
{/* Max Output Lines */}
<div>
<label className="block text-xs font-bold opacity-70 uppercase mb-2">
Max Output Lines per Response
</label>
<ToggleButtonGroup
options={[
{ value: 15 },
{ value: 25 },
{ value: 50 },
{ value: 100 },
{ value: Infinity, label: 'All' },
]}
value={props.maxOutputLines}
onChange={props.setMaxOutputLines}
theme={theme}
/>
<p className="text-xs opacity-50 mt-2">
Long outputs will be collapsed into a scrollable window. Set to "All" to always
show full output.
</p>
</div>
{/* Default Shell */}
<div>
<label className="block text-xs font-bold opacity-70 uppercase mb-1 flex items-center gap-2">
@@ -1662,181 +1648,6 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro
theme={theme}
/>
{/* Context Window Warnings */}
<div>
<label className="block text-xs font-bold opacity-70 uppercase mb-2 flex items-center gap-2">
<AlertTriangle className="w-3 h-3" />
Context Window Warnings
</label>
<div
className="p-3 rounded border space-y-3"
style={{ borderColor: theme.colors.border, backgroundColor: theme.colors.bgMain }}
>
{/* Enable/Disable Toggle */}
<div
className="flex items-center justify-between cursor-pointer"
onClick={() =>
updateContextManagementSettings({
contextWarningsEnabled: !contextManagementSettings.contextWarningsEnabled,
})
}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
updateContextManagementSettings({
contextWarningsEnabled: !contextManagementSettings.contextWarningsEnabled,
});
}
}}
>
<div className="flex-1 pr-3">
<div className="font-medium" style={{ color: theme.colors.textMain }}>
Show context consumption warnings
</div>
<div
className="text-xs opacity-50 mt-0.5"
style={{ color: theme.colors.textDim }}
>
Display warning banners when context window usage reaches configurable
thresholds
</div>
</div>
<button
onClick={(e) => {
e.stopPropagation();
updateContextManagementSettings({
contextWarningsEnabled: !contextManagementSettings.contextWarningsEnabled,
});
}}
className="relative w-10 h-5 rounded-full transition-colors flex-shrink-0"
style={{
backgroundColor: contextManagementSettings.contextWarningsEnabled
? theme.colors.accent
: theme.colors.bgActivity,
}}
role="switch"
aria-checked={contextManagementSettings.contextWarningsEnabled}
>
<span
className={`absolute left-0 top-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
contextManagementSettings.contextWarningsEnabled
? 'translate-x-5'
: 'translate-x-0.5'
}`}
/>
</button>
</div>
{/* Threshold Sliders (ghosted when disabled) */}
<div
className="space-y-4 pt-3 border-t"
style={{
borderColor: theme.colors.border,
opacity: contextManagementSettings.contextWarningsEnabled ? 1 : 0.4,
pointerEvents: contextManagementSettings.contextWarningsEnabled
? 'auto'
: 'none',
}}
>
{/* Yellow Warning Threshold */}
<div>
<div className="flex items-center justify-between mb-2">
<label
className="text-xs font-medium flex items-center gap-2"
style={{ color: theme.colors.textMain }}
>
<div
className="w-2.5 h-2.5 rounded-full"
style={{ backgroundColor: '#eab308' }}
/>
Yellow warning threshold
</label>
<span
className="text-xs font-mono px-2 py-0.5 rounded"
style={{ backgroundColor: 'rgba(234, 179, 8, 0.2)', color: '#fde047' }}
>
{contextManagementSettings.contextWarningYellowThreshold}%
</span>
</div>
<input
type="range"
min={30}
max={90}
step={5}
value={contextManagementSettings.contextWarningYellowThreshold}
onChange={(e) => {
const newYellow = Number(e.target.value);
// Validation: ensure yellow < red by at least 10%
if (newYellow >= contextManagementSettings.contextWarningRedThreshold) {
// Bump red threshold up
updateContextManagementSettings({
contextWarningYellowThreshold: newYellow,
contextWarningRedThreshold: Math.min(95, newYellow + 10),
});
} else {
updateContextManagementSettings({
contextWarningYellowThreshold: newYellow,
});
}
}}
className="w-full h-2 rounded-lg appearance-none cursor-pointer"
style={{
background: `linear-gradient(to right, #eab308 0%, #eab308 ${((contextManagementSettings.contextWarningYellowThreshold - 30) / 60) * 100}%, ${theme.colors.bgActivity} ${((contextManagementSettings.contextWarningYellowThreshold - 30) / 60) * 100}%, ${theme.colors.bgActivity} 100%)`,
}}
/>
</div>
{/* Red Warning Threshold */}
<div>
<div className="flex items-center justify-between mb-2">
<label
className="text-xs font-medium flex items-center gap-2"
style={{ color: theme.colors.textMain }}
>
<div
className="w-2.5 h-2.5 rounded-full"
style={{ backgroundColor: '#ef4444' }}
/>
Red warning threshold
</label>
<span
className="text-xs font-mono px-2 py-0.5 rounded"
style={{ backgroundColor: 'rgba(239, 68, 68, 0.2)', color: '#fca5a5' }}
>
{contextManagementSettings.contextWarningRedThreshold}%
</span>
</div>
<input
type="range"
min={50}
max={95}
step={5}
value={contextManagementSettings.contextWarningRedThreshold}
onChange={(e) => {
const newRed = Number(e.target.value);
// Validation: ensure red > yellow by at least 10%
if (newRed <= contextManagementSettings.contextWarningYellowThreshold) {
// Bump yellow threshold down
updateContextManagementSettings({
contextWarningRedThreshold: newRed,
contextWarningYellowThreshold: Math.max(30, newRed - 10),
});
} else {
updateContextManagementSettings({ contextWarningRedThreshold: newRed });
}
}}
className="w-full h-2 rounded-lg appearance-none cursor-pointer"
style={{
background: `linear-gradient(to right, #ef4444 0%, #ef4444 ${((contextManagementSettings.contextWarningRedThreshold - 50) / 45) * 100}%, ${theme.colors.bgActivity} ${((contextManagementSettings.contextWarningRedThreshold - 50) / 45) * 100}%, ${theme.colors.bgActivity} 100%)`,
}}
/>
</div>
</div>
</div>
</div>
{/* Stats Data Management */}
<div>
<label className="block text-xs font-bold opacity-70 uppercase mb-2 flex items-center gap-2">
@@ -2037,89 +1848,6 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro
</div>
</div>
{/* Document Graph Settings */}
<div>
<label className="block text-xs font-bold opacity-70 uppercase mb-2 flex items-center gap-2">
<Sparkles className="w-3 h-3" />
Document Graph
<span
className="px-1.5 py-0.5 rounded text-[9px] font-bold uppercase"
style={{
backgroundColor: theme.colors.warning + '30',
color: theme.colors.warning,
}}
>
Beta
</span>
</label>
<div
className="p-3 rounded border space-y-3"
style={{ borderColor: theme.colors.border, backgroundColor: theme.colors.bgMain }}
>
{/* Show External Links */}
<div className="flex items-center justify-between">
<div>
<p className="text-sm" style={{ color: theme.colors.textMain }}>
Show external links by default
</p>
<p className="text-xs opacity-50 mt-0.5">
Display external website links as nodes. Can be toggled in the graph view.
</p>
</div>
<button
onClick={() =>
setDocumentGraphShowExternalLinks(!documentGraphShowExternalLinks)
}
className="relative w-10 h-5 rounded-full transition-colors"
style={{
backgroundColor: documentGraphShowExternalLinks
? theme.colors.accent
: theme.colors.bgActivity,
}}
role="switch"
aria-checked={documentGraphShowExternalLinks}
>
<span
className={`absolute left-0 top-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
documentGraphShowExternalLinks ? 'translate-x-5' : 'translate-x-0.5'
}`}
/>
</button>
</div>
{/* Max Nodes */}
<div>
<label className="block text-xs opacity-60 mb-2">
Maximum nodes to display
</label>
<div className="flex items-center gap-3">
<input
type="range"
min={50}
max={1000}
step={50}
value={documentGraphMaxNodes}
onChange={(e) => setDocumentGraphMaxNodes(Number(e.target.value))}
className="flex-1 h-2 rounded-lg appearance-none cursor-pointer"
style={{
background: `linear-gradient(to right, ${theme.colors.accent} 0%, ${theme.colors.accent} ${((documentGraphMaxNodes - 50) / 950) * 100}%, ${theme.colors.bgActivity} ${((documentGraphMaxNodes - 50) / 950) * 100}%, ${theme.colors.bgActivity} 100%)`,
}}
/>
<span
className="text-sm font-mono w-12 text-right"
style={{ color: theme.colors.textMain }}
>
{documentGraphMaxNodes}
</span>
</div>
<p className="text-xs opacity-50 mt-1">
Limits initial graph size for performance. Use &quot;Load more&quot; to show
additional nodes.
</p>
</div>
</div>
</div>
{/* Settings Storage Location */}
<div>
<label className="block text-xs font-bold opacity-70 uppercase mb-2 flex items-center gap-2">
@@ -2314,6 +2042,291 @@ export const SettingsModal = memo(function SettingsModal(props: SettingsModalPro
</div>
)}
{activeTab === 'display' && (
<div className="space-y-5">
{/* Max Output Lines */}
<div>
<label className="block text-xs font-bold opacity-70 uppercase mb-2">
Max Output Lines per Response
</label>
<ToggleButtonGroup
options={[
{ value: 15 },
{ value: 25 },
{ value: 50 },
{ value: 100 },
{ value: Infinity, label: 'All' },
]}
value={props.maxOutputLines}
onChange={props.setMaxOutputLines}
theme={theme}
/>
<p className="text-xs opacity-50 mt-2">
Long outputs will be collapsed into a scrollable window. Set to "All" to always
show full output.
</p>
</div>
{/* Document Graph Settings */}
<div>
<label className="block text-xs font-bold opacity-70 uppercase mb-2 flex items-center gap-2">
<Sparkles className="w-3 h-3" />
Document Graph
<span
className="px-1.5 py-0.5 rounded text-[9px] font-bold uppercase"
style={{
backgroundColor: theme.colors.warning + '30',
color: theme.colors.warning,
}}
>
Beta
</span>
</label>
<div
className="p-3 rounded border space-y-3"
style={{ borderColor: theme.colors.border, backgroundColor: theme.colors.bgMain }}
>
{/* Show External Links */}
<div className="flex items-center justify-between">
<div>
<p className="text-sm" style={{ color: theme.colors.textMain }}>
Show external links by default
</p>
<p className="text-xs opacity-50 mt-0.5">
Display external website links as nodes. Can be toggled in the graph view.
</p>
</div>
<button
onClick={() =>
setDocumentGraphShowExternalLinks(!documentGraphShowExternalLinks)
}
className="relative w-10 h-5 rounded-full transition-colors"
style={{
backgroundColor: documentGraphShowExternalLinks
? theme.colors.accent
: theme.colors.bgActivity,
}}
role="switch"
aria-checked={documentGraphShowExternalLinks}
>
<span
className={`absolute left-0 top-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
documentGraphShowExternalLinks ? 'translate-x-5' : 'translate-x-0.5'
}`}
/>
</button>
</div>
{/* Max Nodes */}
<div>
<label className="block text-xs opacity-60 mb-2">
Maximum nodes to display
</label>
<div className="flex items-center gap-3">
<input
type="range"
min={50}
max={1000}
step={50}
value={documentGraphMaxNodes}
onChange={(e) => setDocumentGraphMaxNodes(Number(e.target.value))}
className="flex-1 h-2 rounded-lg appearance-none cursor-pointer"
style={{
background: `linear-gradient(to right, ${theme.colors.accent} 0%, ${theme.colors.accent} ${((documentGraphMaxNodes - 50) / 950) * 100}%, ${theme.colors.bgActivity} ${((documentGraphMaxNodes - 50) / 950) * 100}%, ${theme.colors.bgActivity} 100%)`,
}}
/>
<span
className="text-sm font-mono w-12 text-right"
style={{ color: theme.colors.textMain }}
>
{documentGraphMaxNodes}
</span>
</div>
<p className="text-xs opacity-50 mt-1">
Limits initial graph size for performance. Use &quot;Load more&quot; to show
additional nodes.
</p>
</div>
</div>
</div>
{/* Context Window Warnings */}
<div>
<label className="block text-xs font-bold opacity-70 uppercase mb-2 flex items-center gap-2">
<AlertTriangle className="w-3 h-3" />
Context Window Warnings
</label>
<div
className="p-3 rounded border space-y-3"
style={{ borderColor: theme.colors.border, backgroundColor: theme.colors.bgMain }}
>
{/* Enable/Disable Toggle */}
<div
className="flex items-center justify-between cursor-pointer"
onClick={() =>
updateContextManagementSettings({
contextWarningsEnabled: !contextManagementSettings.contextWarningsEnabled,
})
}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
updateContextManagementSettings({
contextWarningsEnabled: !contextManagementSettings.contextWarningsEnabled,
});
}
}}
>
<div className="flex-1 pr-3">
<div className="font-medium" style={{ color: theme.colors.textMain }}>
Show context consumption warnings
</div>
<div
className="text-xs opacity-50 mt-0.5"
style={{ color: theme.colors.textDim }}
>
Display warning banners when context window usage reaches configurable
thresholds
</div>
</div>
<button
onClick={(e) => {
e.stopPropagation();
updateContextManagementSettings({
contextWarningsEnabled: !contextManagementSettings.contextWarningsEnabled,
});
}}
className="relative w-10 h-5 rounded-full transition-colors flex-shrink-0"
style={{
backgroundColor: contextManagementSettings.contextWarningsEnabled
? theme.colors.accent
: theme.colors.bgActivity,
}}
role="switch"
aria-checked={contextManagementSettings.contextWarningsEnabled}
>
<span
className={`absolute left-0 top-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
contextManagementSettings.contextWarningsEnabled
? 'translate-x-5'
: 'translate-x-0.5'
}`}
/>
</button>
</div>
{/* Threshold Sliders (ghosted when disabled) */}
<div
className="space-y-4 pt-3 border-t"
style={{
borderColor: theme.colors.border,
opacity: contextManagementSettings.contextWarningsEnabled ? 1 : 0.4,
pointerEvents: contextManagementSettings.contextWarningsEnabled
? 'auto'
: 'none',
}}
>
{/* Yellow Warning Threshold */}
<div>
<div className="flex items-center justify-between mb-2">
<label
className="text-xs font-medium flex items-center gap-2"
style={{ color: theme.colors.textMain }}
>
<div
className="w-2.5 h-2.5 rounded-full"
style={{ backgroundColor: '#eab308' }}
/>
Yellow warning threshold
</label>
<span
className="text-xs font-mono px-2 py-0.5 rounded"
style={{ backgroundColor: 'rgba(234, 179, 8, 0.2)', color: '#fde047' }}
>
{contextManagementSettings.contextWarningYellowThreshold}%
</span>
</div>
<input
type="range"
min={30}
max={90}
step={5}
value={contextManagementSettings.contextWarningYellowThreshold}
onChange={(e) => {
const newYellow = Number(e.target.value);
// Validation: ensure yellow < red by at least 10%
if (newYellow >= contextManagementSettings.contextWarningRedThreshold) {
// Bump red threshold up
updateContextManagementSettings({
contextWarningYellowThreshold: newYellow,
contextWarningRedThreshold: Math.min(95, newYellow + 10),
});
} else {
updateContextManagementSettings({
contextWarningYellowThreshold: newYellow,
});
}
}}
className="w-full h-2 rounded-lg appearance-none cursor-pointer"
style={{
background: `linear-gradient(to right, #eab308 0%, #eab308 ${((contextManagementSettings.contextWarningYellowThreshold - 30) / 60) * 100}%, ${theme.colors.bgActivity} ${((contextManagementSettings.contextWarningYellowThreshold - 30) / 60) * 100}%, ${theme.colors.bgActivity} 100%)`,
}}
/>
</div>
{/* Red Warning Threshold */}
<div>
<div className="flex items-center justify-between mb-2">
<label
className="text-xs font-medium flex items-center gap-2"
style={{ color: theme.colors.textMain }}
>
<div
className="w-2.5 h-2.5 rounded-full"
style={{ backgroundColor: '#ef4444' }}
/>
Red warning threshold
</label>
<span
className="text-xs font-mono px-2 py-0.5 rounded"
style={{ backgroundColor: 'rgba(239, 68, 68, 0.2)', color: '#fca5a5' }}
>
{contextManagementSettings.contextWarningRedThreshold}%
</span>
</div>
<input
type="range"
min={50}
max={95}
step={5}
value={contextManagementSettings.contextWarningRedThreshold}
onChange={(e) => {
const newRed = Number(e.target.value);
// Validation: ensure red > yellow by at least 10%
if (newRed <= contextManagementSettings.contextWarningYellowThreshold) {
// Bump yellow threshold down
updateContextManagementSettings({
contextWarningRedThreshold: newRed,
contextWarningYellowThreshold: Math.max(30, newRed - 10),
});
} else {
updateContextManagementSettings({ contextWarningRedThreshold: newRed });
}
}}
className="w-full h-2 rounded-lg appearance-none cursor-pointer"
style={{
background: `linear-gradient(to right, #ef4444 0%, #ef4444 ${((contextManagementSettings.contextWarningRedThreshold - 50) / 45) * 100}%, ${theme.colors.bgActivity} ${((contextManagementSettings.contextWarningRedThreshold - 50) / 45) * 100}%, ${theme.colors.bgActivity} 100%)`,
}}
/>
</div>
</div>
</div>
</div>
</div>
)}
{activeTab === 'llm' && FEATURE_FLAGS.LLM_SETTINGS && (
<div className="space-y-5">
<div>