co-mono/packages/coding-agent/src/modes/interactive/components/settings-selector.ts
Mario Zechner b4f7a957c4
Add /settings command with unified settings menu (#312)
* Add /settings command with unified settings menu

- Add SettingsList component to tui package with support for:
  - Inline value cycling (Enter/Space toggles)
  - Submenus for complex selections
  - Selection preservation when returning from submenu

- Add /settings slash command consolidating:
  - Auto-compact (toggle)
  - Show images (toggle)
  - Queue mode (cycle)
  - Hide thinking (toggle)
  - Collapse changelog (toggle)
  - Thinking level (submenu)
  - Theme (submenu with preview)

- Update AGENTS.md to clarify no inline imports rule

Fixes #310

* Add /settings to README slash commands table

* Remove old settings slash commands, consolidate into /settings

- Remove /thinking, /queue, /theme, /autocompact, /show-images commands
- Remove unused selector methods and imports
- Update README references to use /settings
2025-12-25 15:39:42 +01:00

251 lines
6.4 KiB
TypeScript

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<ThinkingLevel, string> = {
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;
queueMode: "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;
onQueueModeChange: (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: "queue-mode",
label: "Queue mode",
description: "How to process queued messages while agent is working",
currentValue: config.queueMode,
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"],
});
}
// 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 "queue-mode":
callbacks.onQueueModeChange(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;
}
}