/**
* @license
* Copyright 2025 Vybestack LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type {
LoadedSettings,
SettingScope,
Settings,
} from '../../config/settings.js';
import { getScopeMessageForSetting } from '../../utils/dialogScopeUtils.js';
import { isDefaultValue } from '../../utils/settingsUtils.js';
import { Colors } from '../colors.js';
import { computeDisplayValueForItem } from './settingsDialogDisplay.js';
import { settingValueColor } from './settingsDialogHelpers.js';
import type { PendingValue, SettingItem } from './settingsDialogTypes.js';
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
import { Box, Text } from 'ink';
import React from 'react';
import { getBorderStyle } from '../contexts/UnicodeRenderingContext.js';
interface TextInputProps {
focus: boolean;
value: string;
placeholder?: string;
}
/**
* Simple text input component for search.
*/
function TextInput({ focus, value, placeholder }: TextInputProps) {
const showPlaceholder = value === '' && placeholder !== undefined;
if (showPlaceholder) {
return {placeholder};
}
const text = value;
const cursorPos = text.length;
const beforeCursor = text.slice(0, cursorPos);
const atCursor = text[cursorPos] ?? ' ';
const afterCursor = text.slice(cursorPos + 1);
return (
<>
{beforeCursor}
{focus && (
{atCursor}
)}
{!focus && {atCursor}}
{afterCursor}
>
);
}
// --- SettingItemRow component ---
interface SettingItemRowProps {
item: SettingItem;
idx: number;
scrollOffset: number;
focusSection: 'settings' | 'scope';
activeSettingIndex: number;
maxLabelOrDescriptionWidth: number;
displayValue: string;
selectedScope: SettingScope;
settings: LoadedSettings;
}
function SettingItemRow({
item,
idx,
scrollOffset,
focusSection,
activeSettingIndex,
maxLabelOrDescriptionWidth,
displayValue,
selectedScope,
settings,
}: SettingItemRowProps) {
const isActive =
focusSection === 'settings' && activeSettingIndex === idx + scrollOffset;
const scopeSettings = settings.forScope(selectedScope).settings;
const shouldBeGreyedOut = isDefaultValue(item.value, scopeSettings);
const scopeMessage = getScopeMessageForSetting(
item.value,
selectedScope,
settings,
);
return (
{isActive ? '●' : ''}
{item.label}
{scopeMessage && {scopeMessage}}
{item.description ?? ''}
{displayValue}
);
}
// --- Layout component ---
interface SettingsDialogLayoutProps {
focusSection: 'settings' | 'scope';
editingKey: string | null;
isSearching: boolean;
searchQuery: string;
subSettingsMode: {
isActive: boolean;
parentKey: string;
parentLabel: string;
};
visibleItems: SettingItem[];
scrollOffset: number;
activeSettingIndex: number;
maxLabelOrDescriptionWidth: number;
showScopeSelection: boolean;
showRestartPrompt: boolean;
selectedScope: SettingScope;
settings: LoadedSettings;
scopeItems: Array<{ key: string; value: SettingScope; label: string }>;
editBuffer: string;
editCursorPos: number;
cursorVisible: boolean;
pendingSettings: Settings;
modifiedSettings: Set;
globalPendingChanges: Map;
handleScopeSelect: (scope: SettingScope) => void;
handleScopeHighlight: (scope: SettingScope) => void;
}
export function SettingsDialogLayout(
props: SettingsDialogLayoutProps,
): React.JSX.Element {
return (
);
}
function SettingsDialogHeader({
focusSection,
editingKey,
subSettingsMode,
isSearching,
searchQuery,
}: Pick<
SettingsDialogLayoutProps,
| 'focusSection'
| 'editingKey'
| 'subSettingsMode'
| 'isSearching'
| 'searchQuery'
>): React.JSX.Element {
const borderColor =
editingKey === null && focusSection === 'settings'
? Colors.AccentGreen
: Colors.Gray;
const title = subSettingsMode.isActive
? `${subSettingsMode.parentLabel} > Settings`
: 'Settings';
return (
<>
{focusSection === 'settings' ? '> ' : ' '}
{title}{' '}
>
);
}
type SettingsListContentProps = Pick<
SettingsDialogLayoutProps,
| 'searchQuery'
| 'visibleItems'
| 'scrollOffset'
| 'focusSection'
| 'activeSettingIndex'
| 'maxLabelOrDescriptionWidth'
| 'selectedScope'
| 'settings'
| 'editingKey'
| 'editBuffer'
| 'editCursorPos'
| 'cursorVisible'
| 'pendingSettings'
| 'modifiedSettings'
| 'globalPendingChanges'
| 'subSettingsMode'
>;
function SettingsListContent(
props: SettingsListContentProps,
): React.JSX.Element {
if (props.searchQuery !== '' && props.visibleItems.length === 0) {
return (
No matches found.
);
}
return (
<>
▲
{props.visibleItems.map((item, idx) => (
))}
▼
>
);
}
function SettingsScopeSection({
showScopeSelection,
focusSection,
scopeItems,
selectedScope,
handleScopeSelect,
handleScopeHighlight,
}: Pick<
SettingsDialogLayoutProps,
| 'showScopeSelection'
| 'focusSection'
| 'scopeItems'
| 'selectedScope'
| 'handleScopeSelect'
| 'handleScopeHighlight'
>): React.JSX.Element {
return (
<>
{showScopeSelection && (
{focusSection === 'scope' ? '> ' : ' '}Apply To
item.value === selectedScope,
)}
onSelect={handleScopeSelect}
onHighlight={handleScopeHighlight}
isFocused={focusSection === 'scope'}
showNumbers={focusSection === 'scope'}
/>
)}
>
);
}
function SettingsDialogFooter({
isSearching,
showScopeSelection,
showRestartPrompt,
}: Pick<
SettingsDialogLayoutProps,
'isSearching' | 'showScopeSelection' | 'showRestartPrompt'
>): React.JSX.Element {
const focusHelp = showScopeSelection
? 'Use Enter to select, Tab to change focus'
: 'Use Enter to select';
const helpText = isSearching
? '(Type to search, Esc to clear, Enter to navigate)'
: `(${focusHelp}, / to search, Esc to close)`;
return (
<>
{helpText}
{showRestartPrompt && (
To see changes, LLxprt Code must be restarted. Press r to exit and
apply changes now.
)}
>
);
}