import type { ThinkingLevel } from "@mariozechner/pi-agent-core"; import { Container, getCapabilities, type SelectItem, SelectList, type SettingItem, SettingsList, Spacer, Text, } from "@mariozechner/pi-tui"; import { getSelectListTheme, getSettingsListTheme, theme } from "../theme/theme.js"; import { DynamicBorder } from "./dynamic-border.js"; const THINKING_DESCRIPTIONS: Record = { off: "No reasoning", minimal: "Very brief reasoning (~1k tokens)", low: "Light reasoning (~2k tokens)", medium: "Moderate reasoning (~8k tokens)", high: "Deep reasoning (~16k tokens)", xhigh: "Maximum reasoning (~32k tokens)", }; export interface SettingsConfig { autoCompact: boolean; showImages: boolean; autoResizeImages: boolean; steeringMode: "all" | "one-at-a-time"; followUpMode: "all" | "one-at-a-time"; thinkingLevel: ThinkingLevel; availableThinkingLevels: ThinkingLevel[]; currentTheme: string; availableThemes: string[]; hideThinkingBlock: boolean; collapseChangelog: boolean; } export interface SettingsCallbacks { onAutoCompactChange: (enabled: boolean) => void; onShowImagesChange: (enabled: boolean) => void; onAutoResizeImagesChange: (enabled: boolean) => void; onSteeringModeChange: (mode: "all" | "one-at-a-time") => void; onFollowUpModeChange: (mode: "all" | "one-at-a-time") => void; onThinkingLevelChange: (level: ThinkingLevel) => void; onThemeChange: (theme: string) => void; onThemePreview?: (theme: string) => void; onHideThinkingBlockChange: (hidden: boolean) => void; onCollapseChangelogChange: (collapsed: boolean) => void; onCancel: () => void; } /** * A submenu component for selecting from a list of options. */ class SelectSubmenu extends Container { private selectList: SelectList; constructor( title: string, description: string, options: SelectItem[], currentValue: string, onSelect: (value: string) => void, onCancel: () => void, onSelectionChange?: (value: string) => void, ) { super(); // Title this.addChild(new Text(theme.bold(theme.fg("accent", title)), 0, 0)); // Description if (description) { this.addChild(new Spacer(1)); this.addChild(new Text(theme.fg("muted", description), 0, 0)); } // Spacer this.addChild(new Spacer(1)); // Select list this.selectList = new SelectList(options, Math.min(options.length, 10), getSelectListTheme()); // Pre-select current value const currentIndex = options.findIndex((o) => o.value === currentValue); if (currentIndex !== -1) { this.selectList.setSelectedIndex(currentIndex); } this.selectList.onSelect = (item) => { onSelect(item.value); }; this.selectList.onCancel = onCancel; if (onSelectionChange) { this.selectList.onSelectionChange = (item) => { onSelectionChange(item.value); }; } this.addChild(this.selectList); // Hint this.addChild(new Spacer(1)); this.addChild(new Text(theme.fg("dim", " Enter to select ยท Esc to go back"), 0, 0)); } handleInput(data: string): void { this.selectList.handleInput(data); } } /** * Main settings selector component. */ export class SettingsSelectorComponent extends Container { private settingsList: SettingsList; constructor(config: SettingsConfig, callbacks: SettingsCallbacks) { super(); const supportsImages = getCapabilities().images; const items: SettingItem[] = [ { id: "autocompact", label: "Auto-compact", description: "Automatically compact context when it gets too large", currentValue: config.autoCompact ? "true" : "false", values: ["true", "false"], }, { id: "steering-mode", label: "Steering mode", description: "Enter while streaming queues steering messages. 'one-at-a-time': deliver one, wait for response. 'all': deliver all at once.", currentValue: config.steeringMode, values: ["one-at-a-time", "all"], }, { id: "follow-up-mode", label: "Follow-up mode", description: "Alt+Enter queues follow-up messages until agent stops. 'one-at-a-time': deliver one, wait for response. 'all': deliver all at once.", currentValue: config.followUpMode, values: ["one-at-a-time", "all"], }, { id: "hide-thinking", label: "Hide thinking", description: "Hide thinking blocks in assistant responses", currentValue: config.hideThinkingBlock ? "true" : "false", values: ["true", "false"], }, { id: "collapse-changelog", label: "Collapse changelog", description: "Show condensed changelog after updates", currentValue: config.collapseChangelog ? "true" : "false", values: ["true", "false"], }, { id: "thinking", label: "Thinking level", description: "Reasoning depth for thinking-capable models", currentValue: config.thinkingLevel, submenu: (currentValue, done) => new SelectSubmenu( "Thinking Level", "Select reasoning depth for thinking-capable models", config.availableThinkingLevels.map((level) => ({ value: level, label: level, description: THINKING_DESCRIPTIONS[level], })), currentValue, (value) => { callbacks.onThinkingLevelChange(value as ThinkingLevel); done(value); }, () => done(), ), }, { id: "theme", label: "Theme", description: "Color theme for the interface", currentValue: config.currentTheme, submenu: (currentValue, done) => new SelectSubmenu( "Theme", "Select color theme", config.availableThemes.map((t) => ({ value: t, label: t, })), currentValue, (value) => { callbacks.onThemeChange(value); done(value); }, () => { // Restore original theme on cancel callbacks.onThemePreview?.(currentValue); done(); }, (value) => { // Preview theme on selection change callbacks.onThemePreview?.(value); }, ), }, ]; // Only show image toggle if terminal supports it if (supportsImages) { // Insert after autocompact items.splice(1, 0, { id: "show-images", label: "Show images", description: "Render images inline in terminal", currentValue: config.showImages ? "true" : "false", values: ["true", "false"], }); } // Image auto-resize toggle (always available, affects both attached and read images) items.splice(supportsImages ? 2 : 1, 0, { id: "auto-resize-images", label: "Auto-resize images", description: "Resize large images to 2000x2000 max for better model compatibility", currentValue: config.autoResizeImages ? "true" : "false", values: ["true", "false"], }); // Add borders this.addChild(new DynamicBorder()); this.settingsList = new SettingsList( items, 10, getSettingsListTheme(), (id, newValue) => { switch (id) { case "autocompact": callbacks.onAutoCompactChange(newValue === "true"); break; case "show-images": callbacks.onShowImagesChange(newValue === "true"); break; case "auto-resize-images": callbacks.onAutoResizeImagesChange(newValue === "true"); break; case "steering-mode": callbacks.onSteeringModeChange(newValue as "all" | "one-at-a-time"); break; case "follow-up-mode": callbacks.onFollowUpModeChange(newValue as "all" | "one-at-a-time"); break; case "hide-thinking": callbacks.onHideThinkingBlockChange(newValue === "true"); break; case "collapse-changelog": callbacks.onCollapseChangelogChange(newValue === "true"); break; } }, callbacks.onCancel, ); this.addChild(this.settingsList); this.addChild(new DynamicBorder()); } getSettingsList(): SettingsList { return this.settingsList; } }