mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 13:03:42 +00:00
* 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
251 lines
6.4 KiB
TypeScript
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;
|
|
}
|
|
}
|